From f2adf3793b23ae65989349ea62218e122f0bd275 Mon Sep 17 00:00:00 2001 From: Aceeri Date: Tue, 16 Aug 2022 13:29:23 -0700 Subject: [PATCH 01/25] Windows as Entities Fix some CI and docs Fix links in docs Replacing `Window_id` with `Entity` Adding `WindowCommandsExtensions` for `Commands` Exploding `Window` into multiple coponents Adding `WindowCommands` to replace methods previously existing on `Window` Adding new `ExitCondition` Updating bevy_window to follow new pattern more Split `change_window` system into multiple system that handle `WindowCommand`s Start reworking `winit_runner()` Add initial sketch of example Adding notes Fully implement spawning of created window Start updating `bevy_render` to new patterns More cleanup of winit main loop minor fix And more tweaks continuing refactor of winit-loop Finish setting up components for new windows cleanup Start attempting to get rendering to compile again Updating `bevy_text` Fixing up `bevy_ui` Cleanup Fixing up `bevy_ui` fixing examples Fixing up more examples Every finally compiles! Fixing runtime bugs Wrangling with timing issues in render Window setup operations need to happen during build-time Further attempts at spawning components in build-time Using system-state apply Almost done with rebasing Fix up mis-merges Fix up event loop usage in winit Try to get multiple windows example to run, need to figure out some todos and such Migrate from using events to components with change detection Make system only create once Make sure to apply create window system state Clean up debug prints Some clean up of unused stuff and comments/debugging Fix up some examples/tests, more documentation, enums instead of marker components Some more clean up Just error on no primary window rather than panic Fix up rest of examples and tests Allow setting center position at runtime and consolidate positioning code Update minimized/maximized based on window state Fix up window state a bit, maybe figure out how to update it? Detect minimized window correctly Clean up getting entry to window map for winit Remove create window command example Fix rebasing mistakes and derive Resource for resources Add convenience method to WindowPosition for centering to a monitor Clean up unused imports Fix up ui focus, don't just give rid of stuff and fix imports Revert some mistakes in merging, clean up more imports Use derive instead of explicit impl for WindowFocus Rename field to window Adding more docs Fill inn the rest of the documentation Clean up multiple windows example Explain window bundle in multiple windows Remove TODOs for helpful panics, make all of the panics more helpful Add TODO for window independent UI Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com> Make extract_windodws private again Remove commented out unused code Use removal of window component to signal closing window, some clean up/renaming Simplify primary window closed system Fix up merging and update PR Tryin to fix up remaining errors Compiles again, maybe not entirely correct Try to figure out ordering issue with handle to winit not existing for rendering Fix extraction of windows Move despawning to PostUpdate because clear trackers is ran in Last Adding back canvas scaling Fixing wasm functionality Wasm now compiles correctly Add required platform-specific code Minor cleanup Make window canvas always on the window bundle so people don't need to do platform specific set up and it should Just Work TM Remove change for RemovedComponents Remove rebasing mess up Rename primary window to initial window Clean up examples a bit Use WindowPlugin for settings instead of mismerged WindowSettings Remove unnecessary added method Revert changes to ecs file Window reference stuff Revert "Rename primary window to initial window" This reverts commit 6ef86da0fbc7de44cb676c2d0b1f1c46836b26fa. Normalize render target n stuff Trim excess filters Some more clean up Clean up comments More review feedback changes, revert mouse motion changes Doc comment for extracted window Update crates/bevy_ui/src/flex/mod.rs Co-authored-by: Alice Cecile Update examples/shader/post_processing.rs Co-authored-by: Alice Cecile Fix one mistaken extract Prevent some panicking Make WindowState a more write-only structure, since we can't really reflect whether the window is minimized or not Always on top component Re-apply cursor grab mode fixes Apply suggestions from code review Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com> Clean up some unused things Clean up a bit more bevy_window clean up Cargo fix a bunch of warnings Clean up exampe imports Initial hit test fall through component Revert cargo.toml and clean up Make always on top/hit test enums to better represent their toggle states Fix up examples around minimizing/hit test/always on top Don't panic if the last window is closed Clean up logging for window closing/opening Simplify is changed a bit by using a hashset instead of manually deduping Remove unnecessary primary window query from camera node Safety comment related to creating surface Gracefully handle closing windows more, some clean up Make exitting info more consistent across exit modes Clean up lifetimes on window component query Remove unnecessary zero resolution check, we don't detect minimization because it is too hard to tell what is considered minimized/occluded/etc. per platform Extra convenience method for resolution Clean up docs, fix ios example Remove 's bound for event writers Fix formatting Fix doc link to WindowBundle Remove partial eq derives so we don't accidentally compare things Fix clippy warnings Suppress clippy warning for too many arguments Improve documentation a bit Unify components into a single component for readability/discoverability Don't panic on no primary, just no surface Fix rendering with new unified component Fix up examples for unified component Comment for multiple windows example Fix up scale factor example Don't apply override when positioning the window Fix resizing of resolutions Clean up, fix remaining examples Unnecessary conversion in ui focus Clean up documentation, add docs for WindowState Docs for minimize/maximize constructors cargo check Map entities for WindowRef Clean up rebasing Formatting, emit WindowCreated event Fix up wasm stuff Revert unsupported changes Remove canvas swap testing Update examples/window/multiple_windows.rs Co-authored-by: Thierry Berger Clean up window state stuff so it works a bit more reliably Fix up wasm Remove unnecessary Component derives, add serialize Serialize for CursorIcon Cargo formatting ReflectSerialize behind flag Formatting again Fix up a bit of lints, unnecessary system state in redraw Fix ios example Unnecessary conversion Remove unnecessary import More unnecessary conversions More conversions Set requested resolution when resized as well Set logical resolution when setting physical Clean up logging Remove logical setting in resizing Apply suggestions from code review Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com> Rename set maximize by backend Remove requested width Builder method for scale factor override Some more convenience methods for resolutions Fix resizing examples --- crates/bevy_render/src/camera/camera.rs | 142 +- .../src/camera/camera_driver_node.rs | 6 +- crates/bevy_render/src/lib.rs | 21 +- crates/bevy_render/src/view/mod.rs | 6 +- crates/bevy_render/src/view/window.rs | 80 +- crates/bevy_text/src/text2d.rs | 21 +- crates/bevy_ui/src/flex/mod.rs | 43 +- crates/bevy_ui/src/focus.rs | 30 +- crates/bevy_ui/src/render/mod.rs | 12 +- crates/bevy_ui/src/widget/text.rs | 16 +- crates/bevy_window/src/cursor.rs | 15 +- crates/bevy_window/src/event.rs | 105 +- crates/bevy_window/src/lib.rs | 101 +- crates/bevy_window/src/raw_handle.rs | 3 +- crates/bevy_window/src/system.rs | 43 +- crates/bevy_window/src/window.rs | 1272 +++++++---------- crates/bevy_winit/src/converters.rs | 4 +- crates/bevy_winit/src/lib.rs | 702 +++++---- crates/bevy_winit/src/system.rs | 277 ++++ crates/bevy_winit/src/web_resize.rs | 9 +- crates/bevy_winit/src/winit_windows.rs | 274 ++-- examples/3d/split_screen.rs | 42 +- examples/app/return_after_run.rs | 8 +- examples/ecs/parallel_query.rs | 16 +- examples/games/contributors.rs | 22 +- examples/input/mouse_grab.rs | 20 +- examples/ios/src/lib.rs | 19 +- .../shader/compute_shader_game_of_life.rs | 5 +- examples/shader/post_processing.rs | 11 +- examples/stress_tests/bevymark.rs | 43 +- examples/stress_tests/many_buttons.rs | 6 +- examples/stress_tests/many_cubes.rs | 6 +- examples/stress_tests/many_foxes.rs | 8 +- examples/stress_tests/many_lights.rs | 11 +- examples/stress_tests/many_sprites.rs | 6 +- examples/ui/text_debug.rs | 6 +- examples/ui/window_fallthrough.rs | 31 +- examples/window/low_power.rs | 6 +- examples/window/multiple_windows.rs | 56 +- examples/window/scale_factor_override.rs | 52 +- examples/window/transparent_window.rs | 9 +- examples/window/window_resizing.rs | 15 +- examples/window/window_settings.rs | 79 +- tests/window/minimising.rs | 20 +- tests/window/resizing.rs | 35 +- 45 files changed, 1944 insertions(+), 1770 deletions(-) create mode 100644 crates/bevy_winit/src/system.rs diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index dc6901d8c2e3f..53252ff453588 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -13,6 +13,7 @@ use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, + prelude::With, reflect::ReflectComponent, system::{Commands, Query, Res}, }; @@ -21,7 +22,10 @@ use bevy_reflect::prelude::*; use bevy_reflect::FromReflect; use bevy_transform::components::GlobalTransform; use bevy_utils::HashSet; -use bevy_window::{WindowCreated, WindowId, WindowResized, Windows}; +use bevy_window::{ + NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, +}; + use std::{borrow::Cow, ops::Range}; use wgpu::{Extent3d, TextureFormat}; @@ -325,10 +329,21 @@ impl CameraRenderGraph { /// The "target" that a [`Camera`] will render to. For example, this could be a [`Window`](bevy_window::Window) /// swapchain or an [`Image`]. -#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, Reflect)] pub enum RenderTarget { /// Window to which the camera's view is rendered. - Window(WindowId), + Window(WindowRef), + /// Image to which the camera's view is rendered. + Image(Handle), +} + +/// Normalized version of the render target. +/// +/// Once we have this we shouldn't need to resolve it down anymore. +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum NormalizedRenderTarget { + /// Window to which the camera's view is rendered. + Window(NormalizedWindowRef), /// Image to which the camera's view is rendered. Image(Handle), } @@ -340,16 +355,28 @@ impl Default for RenderTarget { } impl RenderTarget { + /// Normalize the render target down to a more concrete value, mostly used for equality comparisons. + pub fn normalize(&self, primary_window: Option) -> Option { + match self { + RenderTarget::Window(window_ref) => window_ref + .normalize(primary_window) + .map(NormalizedRenderTarget::Window), + RenderTarget::Image(handle) => Some(NormalizedRenderTarget::Image(handle.clone())), + } + } +} + +impl NormalizedRenderTarget { pub fn get_texture_view<'a>( &self, windows: &'a ExtractedWindows, images: &'a RenderAssets, ) -> Option<&'a TextureView> { match self { - RenderTarget::Window(window_id) => windows - .get(window_id) + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) .and_then(|window| window.swap_chain_texture.as_ref()), - RenderTarget::Image(image_handle) => { + NormalizedRenderTarget::Image(image_handle) => { images.get(image_handle).map(|image| &image.texture_view) } } @@ -362,47 +389,55 @@ impl RenderTarget { images: &'a RenderAssets, ) -> Option { match self { - RenderTarget::Window(window_id) => windows - .get(window_id) + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) .and_then(|window| window.swap_chain_texture_format), - RenderTarget::Image(image_handle) => { + NormalizedRenderTarget::Image(image_handle) => { images.get(image_handle).map(|image| image.texture_format) } } } - pub fn get_render_target_info( + pub fn get_render_target_info<'a>( &self, - windows: &Windows, + resolutions: impl IntoIterator, images: &Assets, ) -> Option { - Some(match self { - RenderTarget::Window(window_id) => { - let window = windows.get(*window_id)?; - RenderTargetInfo { - physical_size: UVec2::new(window.physical_width(), window.physical_height()), - scale_factor: window.scale_factor(), - } - } - RenderTarget::Image(image_handle) => { + match self { + NormalizedRenderTarget::Window(window_ref) => resolutions + .into_iter() + .find(|(entity, _)| *entity == window_ref.entity()) + .map(|(_, window)| RenderTargetInfo { + physical_size: UVec2::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ), + scale_factor: window.resolution.scale_factor(), + }), + NormalizedRenderTarget::Image(image_handle) => { let image = images.get(image_handle)?; let Extent3d { width, height, .. } = image.texture_descriptor.size; - RenderTargetInfo { + Some(RenderTargetInfo { physical_size: UVec2::new(width, height), scale_factor: 1.0, - } + }) } - }) + } } + // Check if this render target is contained in the given changed windows or images. fn is_changed( &self, - changed_window_ids: &[WindowId], + changed_window_ids: &HashSet, changed_image_handles: &HashSet<&Handle>, ) -> bool { match self { - RenderTarget::Window(window_id) => changed_window_ids.contains(window_id), - RenderTarget::Image(image_handle) => changed_image_handles.contains(&image_handle), + NormalizedRenderTarget::Window(window_ref) => { + changed_window_ids.contains(&window_ref.entity()) + } + NormalizedRenderTarget::Image(image_handle) => { + changed_image_handles.contains(&image_handle) + } } } } @@ -431,29 +466,16 @@ pub fn camera_system( mut window_resized_events: EventReader, mut window_created_events: EventReader, mut image_asset_events: EventReader>, - windows: Res, + primary_window: Query>, + windows: Query<(Entity, &Window)>, images: Res>, mut cameras: Query<(&mut Camera, &mut T)>, ) { - let mut changed_window_ids = Vec::new(); - - // Collect all unique window IDs of changed windows by inspecting created windows - for event in window_created_events.iter() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } + let primary_window = primary_window.iter().next(); - // Collect all unique window IDs of changed windows by inspecting resized windows - for event in window_resized_events.iter() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } + let mut changed_window_ids = HashSet::new(); + changed_window_ids.extend(window_created_events.iter().map(|event| event.window)); + changed_window_ids.extend(window_resized_events.iter().map(|event| event.window)); let changed_image_handles: HashSet<&Handle> = image_asset_events .iter() @@ -472,18 +494,18 @@ pub fn camera_system( .as_ref() .map(|viewport| viewport.physical_size); - if camera - .target - .is_changed(&changed_window_ids, &changed_image_handles) - || camera.is_added() - || camera_projection.is_changed() - || camera.computed.old_viewport_size != viewport_size - { - camera.computed.target_info = camera.target.get_render_target_info(&windows, &images); - camera.computed.old_viewport_size = viewport_size; - if let Some(size) = camera.logical_viewport_size() { - camera_projection.update(size.x, size.y); - camera.computed.projection_matrix = camera_projection.get_projection_matrix(); + if let Some(normalized_target) = camera.target.normalize(primary_window) { + if normalized_target.is_changed(&changed_window_ids, &changed_image_handles) + || camera.is_added() + || camera_projection.is_changed() + || camera.computed.old_viewport_size != viewport_size + { + camera.computed.target_info = + normalized_target.get_render_target_info(&windows, &images); + if let Some(size) = camera.logical_viewport_size() { + camera_projection.update(size.x, size.y); + camera.computed.projection_matrix = camera_projection.get_projection_matrix(); + } } } } @@ -491,7 +513,7 @@ pub fn camera_system( #[derive(Component, Debug)] pub struct ExtractedCamera { - pub target: RenderTarget, + pub target: Option, pub physical_viewport_size: Option, pub physical_target_size: Option, pub viewport: Option, @@ -510,7 +532,9 @@ pub fn extract_cameras( &VisibleEntities, )>, >, + primary_window: Extract>>, ) { + let primary_window = primary_window.iter().next(); for (entity, camera, camera_render_graph, transform, visible_entities) in query.iter() { if !camera.is_active { continue; @@ -525,7 +549,7 @@ pub fn extract_cameras( } commands.get_or_spawn(entity).insert(( ExtractedCamera { - target: camera.target.clone(), + target: camera.target.normalize(primary_window), viewport: camera.viewport.clone(), physical_viewport_size: Some(viewport_size), physical_target_size: Some(target_size), diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/camera/camera_driver_node.rs index f57929f30caeb..5280324e736ea 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/camera/camera_driver_node.rs @@ -1,5 +1,5 @@ use crate::{ - camera::{ExtractedCamera, RenderTarget}, + camera::{ExtractedCamera, NormalizedRenderTarget}, render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue}, renderer::RenderContext, view::ExtractedWindows, @@ -52,8 +52,8 @@ impl Node for CameraDriverNode { } previous_order_target = Some(new_order_target); if let Ok((_, camera)) = self.cameras.get_manual(world, entity) { - if let RenderTarget::Window(id) = camera.target { - camera_windows.insert(id); + if let Some(NormalizedRenderTarget::Window(window_ref)) = camera.target { + camera_windows.insert(window_ref.entity()); } graph .run_sub_graph(camera.render_graph.clone(), vec![SlotValue::Entity(entity)])?; diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index bb37fc829a7ad..2cb1868939fd2 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -37,6 +37,7 @@ pub mod prelude { }; } +use bevy_window::{PrimaryWindow, RawHandleWrapper}; use globals::GlobalsPlugin; pub use once_cell; @@ -50,7 +51,7 @@ use crate::{ }; use bevy_app::{App, AppLabel, Plugin}; use bevy_asset::{AddAsset, AssetServer}; -use bevy_ecs::prelude::*; +use bevy_ecs::{prelude::*, system::SystemState}; use bevy_utils::tracing::debug; use std::{ any::TypeId, @@ -138,17 +139,17 @@ impl Plugin for RenderPlugin { .init_asset_loader::() .init_debug_asset_loader::(); + let mut system_state: SystemState>> = + SystemState::new(&mut app.world); + let primary_window = system_state.get(&app.world); + if let Some(backends) = self.wgpu_settings.backends { - let windows = app.world.resource_mut::(); let instance = wgpu::Instance::new(backends); - - let surface = windows - .get_primary() - .and_then(|window| window.raw_handle()) - .map(|wrapper| unsafe { - let handle = wrapper.get_handle(); - instance.create_surface(&handle) - }); + let surface = primary_window.get_single().ok().map(|wrapper| unsafe { + // SAFETY: Plugins should be set up on the main thread. + let handle = wrapper.get_handle(); + instance.create_surface(&handle) + }); let request_adapter_options = wgpu::RequestAdapterOptions { power_preference: self.wgpu_settings.power_preference, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 3bb6479c86e89..c161117ce0638 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -285,10 +285,10 @@ fn prepare_view_targets( ) { let mut textures = HashMap::default(); for (entity, camera, view) in cameras.iter() { - if let Some(target_size) = camera.physical_target_size { + if let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) { if let (Some(out_texture_view), Some(out_texture_format)) = ( - camera.target.get_texture_view(&windows, &images), - camera.target.get_texture_format(&windows, &images), + target.get_texture_view(&windows, &images), + target.get_texture_format(&windows, &images), ) { let size = Extent3d { width: target_size.x, diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index 018f193094522..957455747f1ff 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -7,7 +7,7 @@ use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_utils::{tracing::debug, HashMap, HashSet}; use bevy_window::{ - CompositeAlphaMode, PresentMode, RawHandleWrapper, WindowClosed, WindowId, Windows, + CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed, }; use std::ops::{Deref, DerefMut}; use wgpu::TextureFormat; @@ -40,8 +40,9 @@ impl Plugin for WindowRenderPlugin { } pub struct ExtractedWindow { - pub id: WindowId, - pub raw_handle: Option, + /// An entity that contains the components in [`Window`]. + pub entity: Entity, + pub handle: RawHandleWrapper, pub physical_width: u32, pub physical_height: u32, pub present_mode: PresentMode, @@ -54,11 +55,12 @@ pub struct ExtractedWindow { #[derive(Default, Resource)] pub struct ExtractedWindows { - pub windows: HashMap, + pub primary: Option, + pub windows: HashMap, } impl Deref for ExtractedWindows { - type Target = HashMap; + type Target = HashMap; fn deref(&self) -> &Self::Target { &self.windows @@ -74,36 +76,37 @@ impl DerefMut for ExtractedWindows { fn extract_windows( mut extracted_windows: ResMut, mut closed: Extract>, - windows: Extract>, + windows: Extract)>>, ) { - for window in windows.iter() { + for (entity, window, handle, primary) in windows.iter() { + if primary.is_some() { + extracted_windows.primary = Some(entity); + } + let (new_width, new_height) = ( - window.physical_width().max(1), - window.physical_height().max(1), + window.resolution.physical_width().max(1), + window.resolution.physical_height().max(1), ); - let new_present_mode = window.present_mode(); - let mut extracted_window = - extracted_windows - .entry(window.id()) - .or_insert(ExtractedWindow { - id: window.id(), - raw_handle: window.raw_handle(), - physical_width: new_width, - physical_height: new_height, - present_mode: window.present_mode(), - swap_chain_texture: None, - swap_chain_texture_format: None, - size_changed: false, - present_mode_changed: false, - alpha_mode: window.alpha_mode(), - }); + let mut extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow { + entity, + handle: handle.clone(), + physical_width: new_width, + physical_height: new_height, + present_mode: window.present_mode, + swap_chain_texture: None, + size_changed: false, + swap_chain_texture_format: None, + present_mode_changed: false, + alpha_mode: window.composite_alpha_mode, + }); // NOTE: Drop the swap chain frame here extracted_window.swap_chain_texture = None; extracted_window.size_changed = new_width != extracted_window.physical_width || new_height != extracted_window.physical_height; - extracted_window.present_mode_changed = new_present_mode != extracted_window.present_mode; + extracted_window.present_mode_changed = + window.present_mode != extracted_window.present_mode; if extracted_window.size_changed { debug!( @@ -120,13 +123,14 @@ fn extract_windows( if extracted_window.present_mode_changed { debug!( "Window Present Mode changed from {:?} to {:?}", - extracted_window.present_mode, new_present_mode + extracted_window.present_mode, window.present_mode ); - extracted_window.present_mode = new_present_mode; + extracted_window.present_mode = window.present_mode; } } + for closed_window in closed.iter() { - extracted_windows.remove(&closed_window.id); + extracted_windows.remove(&closed_window.window); } } @@ -137,9 +141,9 @@ struct SurfaceData { #[derive(Resource, Default)] pub struct WindowSurfaces { - surfaces: HashMap, + surfaces: HashMap, /// List of windows that we have already called the initial `configure_surface` for - configured_windows: HashSet, + configured_windows: HashSet, } /// Creates and (re)configures window surfaces, and obtains a swapchain texture for rendering. @@ -173,20 +177,14 @@ pub fn prepare_windows( render_instance: Res, render_adapter: Res, ) { - for window in windows - .windows - .values_mut() - // value of raw_handle is only None in synthetic tests - .filter(|x| x.raw_handle.is_some()) - { + for window in windows.windows.values_mut() { let window_surfaces = window_surfaces.deref_mut(); let surface_data = window_surfaces .surfaces - .entry(window.id) + .entry(window.entity) .or_insert_with(|| unsafe { // NOTE: On some OSes this MUST be called from the main thread. - let surface = render_instance - .create_surface(&window.raw_handle.as_ref().unwrap().get_handle()); + let surface = render_instance.create_surface(&window.handle.get_handle()); let format = *surface .get_supported_formats(&render_adapter) .get(0) @@ -236,7 +234,7 @@ pub fn prepare_windows( }) }; - let not_already_configured = window_surfaces.configured_windows.insert(window.id); + let not_already_configured = window_surfaces.configured_windows.insert(window.entity); let surface = &surface_data.surface; if not_already_configured || window.size_changed || window.present_mode_changed { diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 15a4038ad87e5..907f7207458dc 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -5,6 +5,8 @@ use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, + prelude::With, + query::Changed, reflect::ReflectComponent, system::{Commands, Local, Query, Res, ResMut}, }; @@ -19,7 +21,7 @@ use bevy_render::{ use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlas}; use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; -use bevy_window::{WindowId, WindowScaleFactorChanged, Windows}; +use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use crate::{ Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, @@ -69,7 +71,7 @@ pub struct Text2dBundle { pub fn extract_text2d_sprite( mut extracted_sprites: ResMut, texture_atlases: Extract>>, - windows: Extract>, + windows: Extract>>, text2d_query: Extract< Query<( Entity, @@ -81,7 +83,11 @@ pub fn extract_text2d_sprite( )>, >, ) { - let scale_factor = windows.scale_factor(WindowId::primary()) as f32; + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor() as f32) + .unwrap_or(1.0); for (entity, computed_visibility, text, text_layout_info, anchor, text_transform) in text2d_query.iter() @@ -146,9 +152,9 @@ pub fn update_text2d_layout( mut queue: Local>, mut textures: ResMut>, fonts: Res>, - windows: Res, text_settings: Res, mut font_atlas_warning: ResMut, + windows: Query<&Window, With>, mut scale_factor_changed: EventReader, mut texture_atlases: ResMut>, mut font_atlas_set_storage: ResMut>, @@ -162,7 +168,12 @@ pub fn update_text2d_layout( ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.iter().last().is_some(); - let scale_factor = windows.scale_factor(WindowId::primary()); + + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor()) + .unwrap_or(1.0); for (entity, text, bounds, text_layout_info) in &mut text_query { if factor_changed || text.is_changed() || queue.remove(&entity) { diff --git a/crates/bevy_ui/src/flex/mod.rs b/crates/bevy_ui/src/flex/mod.rs index 94c8e37b812ba..8f058dd5887ae 100644 --- a/crates/bevy_ui/src/flex/mod.rs +++ b/crates/bevy_ui/src/flex/mod.rs @@ -13,7 +13,7 @@ use bevy_log::warn; use bevy_math::Vec2; use bevy_transform::components::Transform; use bevy_utils::HashMap; -use bevy_window::{Window, WindowId, WindowScaleFactorChanged, Windows}; +use bevy_window::{PrimaryWindow, Window, WindowResolution, WindowScaleFactorChanged}; use std::fmt; use taffy::{ prelude::{AvailableSpace, Size}, @@ -23,7 +23,7 @@ use taffy::{ #[derive(Resource)] pub struct FlexSurface { entity_to_taffy: HashMap, - window_nodes: HashMap, + window_nodes: HashMap, taffy: Taffy, } @@ -36,7 +36,6 @@ unsafe impl Sync for FlexSurface {} fn _assert_send_sync_flex_surface_impl_safe() { fn _assert_send_sync() {} _assert_send_sync::>(); - _assert_send_sync::>(); // FIXME https://github.com/DioxusLabs/taffy/issues/146 // _assert_send_sync::(); } @@ -145,11 +144,11 @@ without UI components as a child of an entity with UI components, results may be } } - pub fn update_window(&mut self, window: &Window) { + pub fn update_window(&mut self, window: Entity, window_resolution: &WindowResolution) { let taffy = &mut self.taffy; let node = self .window_nodes - .entry(window.id()) + .entry(window) .or_insert_with(|| taffy.new_leaf(taffy::style::Style::default()).unwrap()); taffy @@ -157,8 +156,12 @@ without UI components as a child of an entity with UI components, results may be *node, taffy::style::Style { size: taffy::geometry::Size { - width: taffy::style::Dimension::Points(window.physical_width() as f32), - height: taffy::style::Dimension::Points(window.physical_height() as f32), + width: taffy::style::Dimension::Points( + window_resolution.physical_width() as f32 + ), + height: taffy::style::Dimension::Points( + window_resolution.physical_height() as f32, + ), }, ..Default::default() }, @@ -168,10 +171,10 @@ without UI components as a child of an entity with UI components, results may be pub fn set_window_children( &mut self, - window_id: WindowId, + parent_window: Entity, children: impl Iterator, ) { - let taffy_node = self.window_nodes.get(&window_id).unwrap(); + let taffy_node = self.window_nodes.get(&parent_window).unwrap(); let child_nodes = children .map(|e| *self.entity_to_taffy.get(&e).unwrap()) .collect::>(); @@ -218,7 +221,8 @@ pub enum FlexError { #[allow(clippy::too_many_arguments)] pub fn flex_node_system( - windows: Res, + primary_window: Query<(Entity, &Window), With>, + windows: Query<(Entity, &Window)>, ui_scale: Res, mut scale_factor_events: EventReader, mut flex_surface: ResMut, @@ -234,13 +238,20 @@ pub fn flex_node_system( mut node_transform_query: Query<(Entity, &mut Node, &mut Transform, Option<&Parent>)>, removed_nodes: RemovedComponents, ) { + // assume one window for time being... + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let (primary_window_entity, logical_to_physical_factor) = + if let Ok((entity, primary_window)) = primary_window.get_single() { + (entity, primary_window.resolution.scale_factor()) + } else { + return; + }; + // update window root nodes - for window in windows.iter() { - flex_surface.update_window(window); + for (entity, window) in windows.iter() { + flex_surface.update_window(entity, &window.resolution); } - // assume one window for time being... - let logical_to_physical_factor = windows.scale_factor(WindowId::primary()); let scale_factor = logical_to_physical_factor * ui_scale.scale; fn update_changed( @@ -273,9 +284,7 @@ pub fn flex_node_system( flex_surface.remove_entities(&removed_nodes); // update window children (for now assuming all Nodes live in the primary window) - if let Some(primary_window) = windows.get_primary() { - flex_surface.set_window_children(primary_window.id(), root_node_query.iter()); - } + flex_surface.set_window_children(primary_window_entity, root_node_query.iter()); // update and remove children for entity in &removed_children { diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 0c71fe790e770..7adea1365d5b2 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -3,7 +3,7 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - prelude::Component, + prelude::{Component, With}, query::WorldQuery, reflect::ReflectComponent, system::{Local, Query, Res}, @@ -11,10 +11,10 @@ use bevy_ecs::{ use bevy_input::{mouse::MouseButton, touch::Touches, Input}; use bevy_math::Vec2; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_render::camera::{Camera, RenderTarget}; -use bevy_render::view::ComputedVisibility; +use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::ComputedVisibility}; use bevy_transform::components::GlobalTransform; -use bevy_window::Windows; + +use bevy_window::{PrimaryWindow, Window}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -129,15 +129,19 @@ pub struct NodeQuery { /// The system that sets Interaction for all UI elements based on the mouse cursor activity /// /// Entities with a hidden [`ComputedVisibility`] are always treated as released. +#[allow(clippy::too_many_arguments)] pub fn ui_focus_system( mut state: Local, camera: Query<(&Camera, Option<&UiCameraConfig>)>, - windows: Res, + windows: Query<&Window>, mouse_button_input: Res>, touches_input: Res, ui_stack: Res, mut node_query: Query, + primary_window: Query>, ) { + let primary_window = primary_window.iter().next(); + // reset entities that were both clicked and released in the last frame for entity in state.entities_to_reset.drain(..) { if let Ok(mut interaction) = node_query.get_component_mut::(entity) { @@ -167,18 +171,20 @@ pub fn ui_focus_system( .iter() .filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui)) .filter_map(|(camera, _)| { - if let RenderTarget::Window(window_id) = camera.target { + if let Some(NormalizedRenderTarget::Window(window_id)) = + camera.target.normalize(primary_window) + { Some(window_id) } else { None } }) - .filter_map(|window_id| windows.get(window_id)) - .filter(|window| window.is_focused()) - .find_map(|window| { - window.cursor_position().map(|mut cursor_pos| { - cursor_pos.y = window.height() - cursor_pos.y; - cursor_pos + .find_map(|window_ref| { + windows.get(window_ref.entity()).ok().and_then(|window| { + window.cursor_position.map(|mut cursor_pos| { + cursor_pos.y = window.resolution.height() as f64 - cursor_pos.y; + cursor_pos.as_vec2() + }) }) }) .or_else(|| touches_input.first_pressed_position()); diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index e7dcf453f474e..4ff8bf66a0179 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -2,6 +2,7 @@ mod pipeline; mod render_pass; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; +use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; @@ -29,7 +30,6 @@ use bevy_text::{Text, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; use bevy_utils::FloatOrd; use bevy_utils::HashMap; -use bevy_window::{WindowId, Windows}; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -187,6 +187,7 @@ pub fn extract_uinodes( mut extracted_uinodes: ResMut, images: Extract>>, ui_stack: Extract>, + windows: Extract>>, uinode_query: Extract< Query<( &Node, @@ -297,7 +298,7 @@ pub fn extract_default_ui_camera_view( pub fn extract_text_uinodes( mut extracted_uinodes: ResMut, texture_atlases: Extract>>, - windows: Extract>, + windows: Extract>>, ui_stack: Extract>, uinode_query: Extract< Query<( @@ -310,7 +311,12 @@ pub fn extract_text_uinodes( )>, >, ) { - let scale_factor = windows.scale_factor(WindowId::primary()) as f32; + // TODO: Support window-independent UI scale: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor() as f32) + .unwrap_or(1.0); + for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { if let Ok((uinode, global_transform, text, text_layout_info, visibility, clip)) = uinode_query.get(*entity) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 8455a9455af13..c2694f3e142a1 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -12,7 +12,7 @@ use bevy_text::{ Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, TextSettings, YAxisOrientation, }; -use bevy_window::Windows; +use bevy_window::{PrimaryWindow, Window}; #[derive(Debug, Default)] pub struct QueuedText { @@ -51,7 +51,7 @@ pub fn text_system( mut last_scale_factor: Local, mut textures: ResMut>, fonts: Res>, - windows: Res, + windows: Query<&Window, With>, text_settings: Res, mut font_atlas_warning: ResMut, ui_scale: Res, @@ -69,13 +69,11 @@ pub fn text_system( )>, )>, ) { - // TODO: This should support window-independent scale settings. - // See https://github.com/bevyengine/bevy/issues/5621 - let scale_factor = if let Some(window) = windows.get_primary() { - window.scale_factor() * ui_scale.scale - } else { - ui_scale.scale - }; + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor()) + .unwrap_or(ui_scale.scale); let inv_scale_factor = 1. / scale_factor; diff --git a/crates/bevy_window/src/cursor.rs b/crates/bevy_window/src/cursor.rs index de1fa563ba96b..17e4dce56b892 100644 --- a/crates/bevy_window/src/cursor.rs +++ b/crates/bevy_window/src/cursor.rs @@ -1,12 +1,23 @@ +use bevy_reflect::{prelude::ReflectDefault, FromReflect, Reflect}; + +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + /// The icon to display for a window's cursor. /// /// Examples of all of these cursors can be found [here](https://www.w3schools.com/cssref/playit.asp?filename=playcss_cursor). /// This `enum` is simply a copy of a similar `enum` found in [`winit`](https://docs.rs/winit/latest/winit/window/enum.CursorIcon.html). /// `winit`, in turn, mostly copied cursor types available in the browser. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Default, Debug, Hash, PartialEq, Eq, Clone, Copy, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] pub enum CursorIcon { /// The platform-dependent default cursor. + #[default] Default, /// A simple crosshair. Crosshair, diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 6c8b0e10ba8f5..0141420020811 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use super::{WindowDescriptor, WindowId}; +use bevy_ecs::entity::Entity; use bevy_math::{IVec2, Vec2}; use bevy_reflect::{FromReflect, Reflect}; @@ -16,26 +16,15 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; reflect(Serialize, Deserialize) )] pub struct WindowResized { - pub id: WindowId, + /// Window that has changed. + pub window: Entity, /// The new logical width of the window. pub width: f32, /// The new logical height of the window. pub height: f32, } -/// An event that indicates that a new window should be created. -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] -#[reflect(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub struct CreateWindow { - pub id: WindowId, - pub descriptor: WindowDescriptor, -} - +// TODO: This would redraw all windows ? If yes, update docs to reflect this /// An event that indicates the window should redraw, even if its control flow is set to `Wait` and /// there have been no window events. #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] @@ -49,8 +38,7 @@ pub struct RequestRedraw; /// 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. +/// To create a new window, spawn an entity with a [`crate::Window`] on it. #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[reflect(Debug, PartialEq)] #[cfg_attr( @@ -59,20 +47,20 @@ pub struct RequestRedraw; reflect(Serialize, Deserialize) )] pub struct WindowCreated { - pub id: WindowId, + /// Window that has been created. + pub window: Entity, } /// 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`]. +/// 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, PartialEq, Eq, Reflect, FromReflect)] #[reflect(Debug, PartialEq)] #[cfg_attr( @@ -81,13 +69,12 @@ pub struct WindowCreated { reflect(Serialize, Deserialize) )] pub struct WindowCloseRequested { - pub id: WindowId, + /// Window to close. + pub window: Entity, } -/// 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 +/// An event that is sent whenever a window is closed. This will be sent when +/// the window entity loses its `Window` component or is despawned. #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[reflect(Debug, PartialEq)] #[cfg_attr( @@ -96,10 +83,13 @@ pub struct WindowCloseRequested { reflect(Serialize, Deserialize) )] pub struct WindowClosed { - pub id: WindowId, + /// Window that has been closed. + /// + /// Note that this entity probably no longer exists + /// by the time this event is received. + pub window: Entity, } - -/// An event reporting that the mouse cursor has moved on a window. +/// An event reporting that the mouse cursor has moved inside a window. /// /// The event is sent only if the cursor is over one of the application's windows. /// It is the translated version of [`WindowEvent::CursorMoved`] from the `winit` crate. @@ -116,10 +106,9 @@ pub struct WindowClosed { reflect(Serialize, Deserialize) )] pub struct CursorMoved { - /// The identifier of the window the cursor has moved on. - pub id: WindowId, - - /// The position of the cursor, in window coordinates. + /// Window that the cursor moved inside. + pub window: Entity, + /// The cursor position in logical pixels. pub position: Vec2, } @@ -132,7 +121,8 @@ pub struct CursorMoved { reflect(Serialize, Deserialize) )] pub struct CursorEntered { - pub id: WindowId, + /// Window that the cursor entered. + pub window: Entity, } /// An event that is sent whenever the user's cursor leaves a window. @@ -144,7 +134,8 @@ pub struct CursorEntered { reflect(Serialize, Deserialize) )] pub struct CursorLeft { - pub id: WindowId, + /// Window that the cursor left. + pub window: Entity, } /// An event that is sent whenever a window receives a character from the OS or underlying system. @@ -156,7 +147,9 @@ pub struct CursorLeft { reflect(Serialize, Deserialize) )] pub struct ReceivedCharacter { - pub id: WindowId, + /// Window that received the character. + pub window: Entity, + /// Received character. pub char: char, } @@ -169,7 +162,9 @@ pub struct ReceivedCharacter { reflect(Serialize, Deserialize) )] pub struct WindowFocused { - pub id: WindowId, + /// Window that changed focus. + pub window: Entity, + /// Whether it was focused (true) or lost focused (false). pub focused: bool, } @@ -182,7 +177,9 @@ pub struct WindowFocused { reflect(Serialize, Deserialize) )] pub struct WindowScaleFactorChanged { - pub id: WindowId, + /// Window that had it's scale factor changed. + pub window: Entity, + /// The new scale factor. pub scale_factor: f64, } @@ -195,7 +192,9 @@ pub struct WindowScaleFactorChanged { reflect(Serialize, Deserialize) )] pub struct WindowBackendScaleFactorChanged { - pub id: WindowId, + /// Window that had it's scale factor changed by the backend. + pub window: Entity, + /// The new scale factor. pub scale_factor: f64, } @@ -208,11 +207,27 @@ pub struct WindowBackendScaleFactorChanged { reflect(Serialize, Deserialize) )] pub enum FileDragAndDrop { - DroppedFile { id: WindowId, path_buf: PathBuf }, - - HoveredFile { id: WindowId, path_buf: PathBuf }, - - HoveredFileCancelled { id: WindowId }, + /// File is being dropped into a window. + DroppedFile { + /// Window the file was dropped into. + window: Entity, + /// Path to the file that was dropped in. + path_buf: PathBuf, + }, + + /// File is currently being hovered over a window. + HoveredFile { + /// Window a file is possibly going to be dropped into. + window: Entity, + /// Path to the file that might be dropped in. + path_buf: PathBuf, + }, + + /// File hovering was cancelled. + HoveredFileCancelled { + /// Window that had a cancelled file drop. + window: Entity, + }, } /// An event that is sent when a window is repositioned in physical pixels. @@ -224,6 +239,8 @@ pub enum FileDragAndDrop { reflect(Serialize, Deserialize) )] pub struct WindowMoved { - pub id: WindowId, + /// Window that moved. + pub entity: Entity, + /// Where the window moved to in physical pixels. pub position: IVec2, } diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 1632c06042d66..df393e5d6e86d 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -4,34 +4,32 @@ mod event; mod raw_handle; mod system; mod window; -mod windows; pub use crate::raw_handle::*; + pub use cursor::*; pub use event::*; pub use system::*; pub use window::*; -pub use windows::*; pub mod prelude { #[doc(hidden)] pub use crate::{ CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, MonitorSelection, - ReceivedCharacter, Window, WindowDescriptor, WindowMode, WindowMoved, WindowPlugin, - WindowPosition, Windows, + ReceivedCharacter, Window, WindowMoved, WindowPosition, WindowResizeConstraints, }; } -use bevy_app::prelude::*; -use bevy_ecs::schedule::{IntoSystemDescriptor, SystemLabel}; use std::path::PathBuf; +use bevy_app::prelude::*; +use bevy_ecs::schedule::SystemLabel; + impl Default for WindowPlugin { fn default() -> Self { WindowPlugin { - window: Default::default(), - add_primary_window: true, - exit_on_all_closed: true, + primary_window: Some(Window::default()), + exit_condition: ExitCondition::OnAllClosed, close_when_requested: true, } } @@ -39,21 +37,26 @@ impl Default for WindowPlugin { /// A [`Plugin`] that defines an interface for windowing support in Bevy. pub struct WindowPlugin { - pub window: WindowDescriptor, - /// Whether to create a window when added. + /// Settings for the primary window. This will be spawned by + /// default, if you want to run without a primary window you should + /// set this to `None`. /// /// 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 primary_window: Option, + /// 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`. + /// surprise your users. It is recommended to leave this setting to + /// either [`ExitCondition::OnAllClosed`] or [`ExitCondition::OnPrimaryClosed`]. /// - /// If true, this plugin will add [`exit_on_all_closed`] to [`CoreStage::PostUpdate`]. - pub exit_on_all_closed: bool, + /// [`ExitCondition::OnAllClosed`] will add [`exit_on_all_closed`] to [`CoreStage::Update`]. + /// [`ExitCondition::OnPrimaryClosed`] will add [`exit_on_primary_closed`] to [`CoreStage::Update`]. + pub exit_condition: ExitCondition, + /// Whether to close windows when they are requested to be closed (i.e. /// when the close button is pressed). /// @@ -65,8 +68,8 @@ pub struct WindowPlugin { impl Plugin for WindowPlugin { fn build(&self, app: &mut App) { + // User convenience events app.add_event::() - .add_event::() .add_event::() .add_event::() .add_event::() @@ -79,29 +82,30 @@ impl Plugin for WindowPlugin { .add_event::() .add_event::() .add_event::() - .add_event::() - .init_resource::(); - - if self.add_primary_window { - app.world.send_event(CreateWindow { - id: WindowId::primary(), - descriptor: self.window.clone(), - }); + .add_event::(); + + if let Some(primary_window) = &self.primary_window { + app.world + .spawn(primary_window.clone()) + .insert(PrimaryWindow); } - if self.exit_on_all_closed { - app.add_system_to_stage( - CoreStage::PostUpdate, - exit_on_all_closed.after(ModifiesWindows), - ); + match self.exit_condition { + ExitCondition::OnPrimaryClosed => { + app.add_system(exit_on_primary_closed); + } + ExitCondition::OnAllClosed => { + app.add_system(exit_on_all_closed); + } + ExitCondition::DontExit => {} } + if self.close_when_requested { - app.add_system(close_when_requested); + app.add_system_to_stage(CoreStage::First, close_when_requested); } // Register event types app.register_type::() - .register_type::() .register_type::() .register_type::() .register_type::() @@ -117,18 +121,43 @@ impl Plugin for WindowPlugin { .register_type::(); // Register window descriptor and related types - app.register_type::() - .register_type::() - .register_type::() - .register_type::() + app.register_type::() + .register_type::() + .register_type::() + .register_type::() .register_type::() + .register_type::() + .register_type::() + .register_type::() .register_type::() - .register_type::(); + .register_type::() + .register_type::(); // Register `PathBuf` as it's used by `FileDragAndDrop` app.register_type::(); } } +/// System Label marking when changes are applied to windows #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub struct ModifiesWindows; + +/// Defines the specific conditions the application should exit on +#[derive(Clone)] +pub enum ExitCondition { + /// Close application when the primary window is closed + /// + /// The plugin will add [`exit_on_primary_closed`] to [`CoreStage::Update`]. + OnPrimaryClosed, + /// Close application when all windows are closed + /// + /// The plugin will add [`exit_on_all_closed`] to [`CoreStage::Update`]. + OnAllClosed, + /// Keep application running headless even after closing all windows + /// + /// If selecting 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. + DontExit, +} diff --git a/crates/bevy_window/src/raw_handle.rs b/crates/bevy_window/src/raw_handle.rs index 0e495d2c138b5..6c535605991f0 100644 --- a/crates/bevy_window/src/raw_handle.rs +++ b/crates/bevy_window/src/raw_handle.rs @@ -1,3 +1,4 @@ +use bevy_ecs::prelude::Component; use raw_window_handle::{ HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, }; @@ -7,7 +8,7 @@ use raw_window_handle::{ /// Depending on the platform, the underlying pointer-containing handle cannot be used on all threads, /// and so we cannot simply make it (or any type that has a safe operation to get a [`RawWindowHandle`] or [`RawDisplayHandle`]) /// thread-safe. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Component)] pub struct RawHandleWrapper { pub window_handle: RawWindowHandle, pub display_handle: RawDisplayHandle, diff --git a/crates/bevy_window/src/system.rs b/crates/bevy_window/src/system.rs index 4cf79fa54997e..0bf953efc4f6a 100644 --- a/crates/bevy_window/src/system.rs +++ b/crates/bevy_window/src/system.rs @@ -1,4 +1,4 @@ -use crate::{Window, WindowCloseRequested, Windows}; +use crate::{PrimaryWindow, Window, WindowCloseRequested}; use bevy_app::AppExit; use bevy_ecs::prelude::*; @@ -11,8 +11,24 @@ use bevy_input::{keyboard::KeyCode, Input}; /// 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, windows: Res) { +pub fn exit_on_all_closed(mut app_exit_events: EventWriter, windows: Query<&Window>) { if windows.iter().count() == 0 { + bevy_utils::tracing::info!("No windows are open, exiting"); + app_exit_events.send(AppExit); + } +} + +/// Exit the application when the primary window has been closed +/// +/// This system is added by the [`WindowPlugin`] +/// +/// [`WindowPlugin`]: crate::WindowPlugin +pub fn exit_on_primary_closed( + mut app_exit_events: EventWriter, + windows: Query<(), (With, With)>, +) { + if windows.is_empty() { + bevy_utils::tracing::info!("Primary windows was closed, exiting"); app_exit_events.send(AppExit); } } @@ -24,22 +40,27 @@ pub fn exit_on_all_closed(mut app_exit_events: EventWriter, windows: Re /// Ensure that you read the caveats documented on that field if doing so. /// /// [`WindowPlugin`]: crate::WindowPlugin -pub fn close_when_requested( - mut windows: ResMut, - mut closed: EventReader, -) { +pub fn close_when_requested(mut commands: Commands, mut closed: EventReader) { for event in closed.iter() { - windows.get_mut(event.id).map(Window::close); + commands.entity(event.window).despawn(); } } /// Close the focused window whenever the escape key (Esc) is pressed /// /// This is useful for examples or prototyping. -pub fn close_on_esc(mut windows: ResMut, input: Res>) { - if input.just_pressed(KeyCode::Escape) { - if let Some(window) = windows.get_focused_mut() { - window.close(); +pub fn close_on_esc( + mut commands: Commands, + focused_windows: Query<(Entity, &Window)>, + input: Res>, +) { + for (window, focus) in focused_windows.iter() { + if !focus.focused { + continue; + } + + if input.just_pressed(KeyCode::Escape) { + commands.entity(window).despawn(); } } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 93adedbe86a71..1df1637a54d38 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,19 +1,89 @@ -use bevy_math::{DVec2, IVec2, UVec2, Vec2}; +use bevy_ecs::{ + entity::{Entity, EntityMap, MapEntities, MapEntitiesError}, + prelude::{Component, ReflectComponent}, +}; +use bevy_math::{DVec2, IVec2}; use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect}; -use bevy_utils::{tracing::warn, Uuid}; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -/// A unique ID for a [`Window`]. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Reflect, FromReflect)] -#[reflect_value(Debug, PartialEq, Hash, Default)] +use bevy_utils::tracing::warn; + +use crate::CursorIcon; + +/// Marker component for the window considered the primary window. +/// +/// Currently this is assumed to only exist on 1 entity at a time. +#[derive(Default, Debug, Component, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Reflect)] +#[reflect(Component)] +pub struct PrimaryWindow; + +/// Reference to a window, whether it be a direct link to a specific entity or +/// a more vague defaulting choice. +#[repr(C)] +#[derive(Default, Copy, Clone, Debug, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum WindowRef { + /// This will be linked to the primary window that is created by default + /// in the [`WindowPlugin`](crate::WindowPlugin::primary_window). + #[default] + Primary, + /// A more direct link to a window entity. + /// + /// Use this if you want to reference a secondary/tertiary/... window. + /// + /// To create a new window you can spawn an entity with a [`Window`], + /// then you can use that entity here for usage in cameras. + Entity(Entity), +} + +impl WindowRef { + /// Normalize the window reference so that it can be compared to other window references. + pub fn normalize(&self, primary_window: Option) -> Option { + let entity = match self { + Self::Primary => primary_window, + Self::Entity(entity) => Some(*entity), + }; + + entity.map(NormalizedWindowRef) + } +} + +impl MapEntities for WindowRef { + fn map_entities(&mut self, entity_map: &EntityMap) -> Result<(), MapEntitiesError> { + match self { + Self::Entity(entity) => { + *entity = entity_map.get(*entity)?; + Ok(()) + } + Self::Primary => Ok(()), + } + } +} + +/// A flattened representation of a window reference for equality/hashing purposes. +/// +/// For most purposes you probably want to use the unnormalized version [`WindowRef`]. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), - reflect_value(Serialize, Deserialize) + reflect(Serialize, Deserialize) )] -pub struct WindowId(Uuid); +pub struct NormalizedWindowRef(Entity); + +impl NormalizedWindowRef { + /// Fetch the entity of this window reference + pub fn entity(&self) -> Entity { + self.0 + } +} /// Presentation mode for a window. /// @@ -27,11 +97,8 @@ pub struct WindowId(Uuid); /// `AutoVsync` or `AutoNoVsync` will gracefully fallback to `Fifo` when unavailable. /// /// `Immediate` or `Mailbox` will panic if not supported by the platform. -/// -/// The presentation mode may be declared in the [`WindowDescriptor`](WindowDescriptor) using [`WindowDescriptor::present_mode`](WindowDescriptor::present_mode) -/// or updated on a [`Window`](Window) using [`set_present_mode`](Window::set_present_mode). #[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect, FromReflect)] +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), @@ -65,12 +132,13 @@ pub enum PresentMode { /// The presentation engine waits for the next vertical blanking period to update /// the current image. The framerate will be capped at the display refresh rate, /// corresponding to the `VSync`. Tearing cannot be observed. Optimal for mobile. + #[default] Fifo = 4, // NOTE: The explicit ordinal values mirror wgpu. } /// Specifies how the alpha channel of the textures should be handled during compositing. #[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, FromReflect)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), @@ -80,6 +148,7 @@ pub enum PresentMode { pub enum CompositeAlphaMode { /// Chooses either `Opaque` or `Inherit` automatically, depending on the /// `alpha_mode` that the current surface can support. + #[default] Auto = 0, /// The alpha channel, if it exists, of the textures is ignored in the /// compositing process. Instead, the textures is treated as if it has a @@ -103,35 +172,119 @@ pub enum CompositeAlphaMode { Inherit = 4, } -impl WindowId { - /// Creates a new [`WindowId`]. - pub fn new() -> Self { - WindowId(Uuid::new_v4()) - } - /// The [`WindowId`] for the primary window. - pub const fn primary() -> Self { - WindowId(Uuid::from_u128(0)) - } - /// Get whether or not this [`WindowId`] is for the primary window. - pub fn is_primary(&self) -> bool { - *self == WindowId::primary() - } +/// Defines the way a window is displayed +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq)] +pub enum WindowMode { + /// Creates a window that uses the given size. + #[default] + Windowed, + /// Creates a borderless window that uses the full size of the screen. + BorderlessFullscreen, + /// Creates a fullscreen window that will render at desktop resolution. The app will use the closest supported size + /// from the given size and scale it to fit the screen. + SizedFullscreen, + /// Creates a fullscreen window that uses the maximum supported size. + Fullscreen, } -use crate::CursorIcon; -use std::fmt; - -use crate::raw_handle::RawHandleWrapper; - -impl fmt::Display for WindowId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.as_simple().fmt(f) - } +/// Define how a window will be created and how it will behave. +#[derive(Component, Debug, Clone, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Component, Default)] +pub struct Window { + /// The cursor of this window. + pub cursor: Cursor, + /// The position of this window's cursor. + pub cursor_position: Option, + /// What presentation mode to give the window. + pub present_mode: PresentMode, + /// Which fullscreen or windowing mode should be used? + pub mode: WindowMode, + /// Where the window should be placed. + pub position: WindowPosition, + /// What resolution the window should have. + pub resolution: WindowResolution, + /// Stores the title of the window. + pub title: String, + /// Should the window start minimized, maximized, normal? + pub state: WindowState, + /// How the alpha channel of textures should be handled while compositing. + pub composite_alpha_mode: CompositeAlphaMode, + /// Which size limits to give the window. + pub resize_constraints: WindowResizeConstraints, + /// Should the window be resizable? + /// + /// Note: This does not stop the program from fullscreening/setting + /// the size programmatically. + pub resizable: bool, + /// Should the window have decorations enabled? + /// + /// (Decorations are the minimize, maximize, and close buttons on desktop apps) + /// + // ## Platform-specific + // + // **`iOS`**, **`Android`**, and the **`Web`** do not have decorations. + pub decorations: bool, + /// Should the window be transparent? + /// + /// Defines whether the background of the window should be transparent. + /// + /// ## Platform-specific + /// - iOS / Android / Web: Unsupported. + /// - macOS X: Not working as expected. + /// - Windows 11: Not working as expected + /// macOS X transparent works with winit out of the box, so this issue might be related to: + /// Windows 11 is related to + pub transparent: bool, + /// Should the window start focused? + pub focused: bool, + /// How the window will interact if it is on a web platform rather than OS window. + pub canvas: WindowCanvas, + /// Should the window always be on top of other windows? + /// + /// ## Platform-specific + /// + /// - iOS / Android / Web / Wayland: Unsupported. + pub always_on_top: bool, + /// Set whether or not mouse events within *this* window are captured or fall through to the Window below. + /// + /// ## Platform-specific + /// + /// - iOS / Android / Web / X11: Unsupported. + pub hit_test: bool, } -impl Default for WindowId { +impl Default for Window { fn default() -> Self { - WindowId::primary() + Self { + title: "Bevy App".to_owned(), + cursor: Default::default(), + cursor_position: Default::default(), + present_mode: Default::default(), + mode: Default::default(), + position: Default::default(), + resolution: Default::default(), + state: Default::default(), + composite_alpha_mode: Default::default(), + canvas: Default::default(), + resize_constraints: Default::default(), + resizable: true, + decorations: true, + transparent: false, + focused: true, + always_on_top: false, + hit_test: true, + } } } @@ -150,9 +303,13 @@ impl Default for WindowId { )] #[reflect(Debug, PartialEq, Default)] pub struct WindowResizeConstraints { + /// The minimum width the window can have. pub min_width: f32, + /// The minimum height the window can have. pub min_height: f32, + /// The maximum width the window can have. pub max_width: f32, + /// The maximum height the window can have. pub max_height: f32, } @@ -168,6 +325,9 @@ impl Default for WindowResizeConstraints { } impl WindowResizeConstraints { + /// Checks if the constraints are valid. + /// + /// Will output warnings if it isn't. #[must_use] pub fn check_constraints(&self) -> Self { let WindowResizeConstraints { @@ -201,10 +361,122 @@ impl WindowResizeConstraints { } } -/// An operating system window that can present content and receive user input. -/// -/// To create a window, use a [`EventWriter`](`crate::CreateWindow`). -/// +/// Stores data about the window's cursor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] +pub struct Cursor { + /// Get the current [`CursorIcon`] while inside the window. + pub icon: CursorIcon, + + /// Whether the cursor is visible or not. + /// + /// ## Platform-specific + /// + /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window. + /// To stop the cursor from leaving the window, change [`Cursor::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`] + /// - **`macOS`**: The cursor is hidden only when the window is focused. + /// - **`iOS`** and **`Android`** do not have cursors + pub visible: bool, + + /// Whether or not the cursor is locked. + /// + /// ## Platform-specific + /// + /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] + /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] + /// - **`iOS/Android`** don't have cursors. + /// + /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. + pub grab_mode: CursorGrabMode, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor { + icon: CursorIcon::Default, + visible: true, + grab_mode: CursorGrabMode::None, + } + } +} + +/// Stores the cursor position of the window. +#[derive(Default, Debug, Clone, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, Default)] +pub struct CursorPosition { + /// Cursor position if it is inside of the window. + physical_cursor_position: Option, +} + +impl CursorPosition { + /// Creates a new [`CursorPosition`]. + pub fn new(physical_cursor_position: Option) -> Self { + Self { + physical_cursor_position, + } + } + + /// The current mouse position, in physical pixels. + #[inline] + pub fn physical_position(&self) -> Option { + self.physical_cursor_position + } + + /// Set the cursor's position, in physical pixels. + pub fn set(&mut self, position: Option) { + self.physical_cursor_position = position; + } +} + +/// Defines where window should be placed at on creation. +#[derive(Default, Debug, Clone, Copy, PartialEq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq)] +pub enum WindowPosition { + /// Position will be set by the window manager + #[default] + Automatic, + /// Window will be centered on the selected monitor + /// + /// Note that this does not account for window decorations. + Centered(MonitorSelection), + /// The window's top-left corner will be placed at the specified position (in pixels) + /// + /// (0,0) represents top-left corner of screen space. + At(IVec2), +} + +impl WindowPosition { + /// Creates a new [`WindowPosition`] at a position. + pub fn new(position: IVec2) -> Self { + Self::At(position) + } + + /// Set the position to a specific point. + pub fn set(&mut self, position: IVec2) { + *self = WindowPosition::At(position); + } + + /// Set the window to a specific monitor. + pub fn center(&mut self, monitor: MonitorSelection) { + *self = WindowPosition::Centered(monitor); + } +} + /// ## Window Sizes /// /// There are three sizes associated with a window. The physical size which is @@ -218,204 +490,50 @@ impl WindowResizeConstraints { /// requested size due to operating system limits on the window size, or the /// quantization of the logical size when converting the physical size to the /// logical size through the scaling factor. -/// -/// ## Accessing a `Window` from a system -/// -/// To access a `Window` from a system, use [`bevy_ecs::change_detection::ResMut`]`<`[`crate::Windows`]`>`. -/// -/// ### Example -/// ```no_run -/// # use bevy_app::App; -/// # use bevy_window::Windows; -/// # use bevy_ecs::change_detection::ResMut; -/// # fn main(){ -/// # App::new().add_system(access_window_system).run(); -/// # } -/// fn access_window_system(mut windows: ResMut){ -/// for mut window in windows.iter_mut() { -/// window.set_title(String::from("Yay, I'm a window!")); -/// } -/// } -/// ``` -/// To test code that uses `Window`s, one can test it with varying `Window` parameters by -/// creating `WindowResizeConstraints` or `WindowDescriptor` structures. -/// values by setting -/// -/// ``` -/// # use bevy_utils::default; -/// # use bevy_window::{Window, WindowCommand, WindowDescriptor, WindowId, WindowResizeConstraints}; -/// # fn compute_window_area(w: &Window) -> f32 { -/// # w.width() * w.height() -/// # } -/// # fn grow_window_to_text_size(_window: &mut Window, _text: &str) {} -/// # fn set_new_title(window: &mut Window, text: String) { window.set_title(text); } -/// # fn a_window_resize_test() { -/// let resize_constraints = WindowResizeConstraints { -/// min_width: 400.0, -/// min_height: 300.0, -/// max_width: 1280.0, -/// max_height: 1024.0, -/// }; -/// let window_descriptor = WindowDescriptor { -/// width: 800.0, -/// height: 600.0, -/// resizable: true, -/// resize_constraints, -/// ..default() -/// }; -/// let mut window = Window::new( -/// WindowId::new(), -/// &window_descriptor, -/// 100, // physical_width -/// 100, // physical_height -/// 1.0, // scale_factor -/// None, None); -/// -/// let area = compute_window_area(&window); -/// assert_eq!(area, 100.0 * 100.0); -/// -/// grow_window_to_text_size(&mut window, "very long text that does not wrap"); -/// assert_eq!(window.physical_width(), window.requested_width() as u32); -/// grow_window_to_text_size(&mut window, "very long text that does wrap, creating a maximum width window"); -/// assert_eq!(window.physical_width(), window.requested_width() as u32); -/// -/// set_new_title(&mut window, "new title".to_string()); -/// let mut found_command = false; -/// for command in window.drain_commands() { -/// if command == (WindowCommand::SetTitle{ title: "new title".to_string() }) { -/// found_command = true; -/// break; -/// } -/// } -/// assert_eq!(found_command, true); -/// } -/// ``` -#[derive(Debug)] -pub struct Window { - id: WindowId, - requested_width: f32, - requested_height: f32, +#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] +pub struct WindowResolution { physical_width: u32, physical_height: u32, - resize_constraints: WindowResizeConstraints, - position: Option, scale_factor_override: Option, - backend_scale_factor: f64, - title: String, - present_mode: PresentMode, - resizable: bool, - decorations: bool, - cursor_icon: CursorIcon, - cursor_visible: bool, - cursor_grab_mode: CursorGrabMode, - hittest: bool, - physical_cursor_position: Option, - raw_handle: Option, - focused: bool, - mode: WindowMode, - canvas: Option, - fit_canvas_to_parent: bool, - command_queue: Vec, - alpha_mode: CompositeAlphaMode, - always_on_top: bool, + scale_factor: f64, } -/// A command to be sent to a window. -/// -/// Bevy apps don't interact with this `enum` directly. Instead, they should use the methods on [`Window`]. -/// This `enum` is meant for authors of windowing plugins. See the documentation on [`crate::WindowPlugin`] for more information. -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub enum WindowCommand { - /// Set the window's [`WindowMode`]. - SetWindowMode { - mode: WindowMode, - resolution: UVec2, - }, - /// Set the window's title. - SetTitle { - title: String, - }, - /// Set the window's scale factor. - SetScaleFactor { - scale_factor: f64, - }, - /// Set the window's resolution. - SetResolution { - logical_resolution: Vec2, - scale_factor: f64, - }, - /// Set the window's [`PresentMode`]. - SetPresentMode { - present_mode: PresentMode, - }, - /// Set whether or not the window is resizable. - SetResizable { - resizable: bool, - }, - /// Set whether or not the window has decorations. - /// - /// Examples of decorations include the close, full screen, and minimize buttons - SetDecorations { - decorations: bool, - }, - /// Set whether or not the cursor's position is locked. - SetCursorGrabMode { - grab_mode: CursorGrabMode, - }, - /// Set the cursor's [`CursorIcon`]. - SetCursorIcon { - icon: CursorIcon, - }, - /// Set whether or not the cursor is visible. - SetCursorVisibility { - visible: bool, - }, - /// Set the cursor's position. - SetCursorPosition { - position: Vec2, - }, - /// Set whether or not mouse events within *this* window are captured, or fall through to the Window below. - SetCursorHitTest { - hittest: bool, - }, - /// Set whether or not the window is maximized. - SetMaximized { - maximized: bool, - }, - /// Set whether or not the window is minimized. - SetMinimized { - minimized: bool, - }, - /// Set the window's position on the selected monitor. - SetPosition { - monitor_selection: MonitorSelection, - position: IVec2, - }, - /// Sets the position of the window to be in the center of the selected monitor. - Center(MonitorSelection), - /// Set the window's [`WindowResizeConstraints`] - SetResizeConstraints { - resize_constraints: WindowResizeConstraints, - }, - /// Set whether the window is always on top. - SetAlwaysOnTop { - always_on_top: bool, - }, - Close, + +impl Default for WindowResolution { + fn default() -> Self { + WindowResolution { + physical_width: 1280, + physical_height: 720, + scale_factor_override: None, + scale_factor: 1.0, + } + } } /// Defines if and how the cursor is grabbed. /// -/// Use this enum with [`Window::set_cursor_grab_mode`] to grab the cursor. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] +/// ## Platform-specific +/// +/// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] +/// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] +/// - **`iOS/Android`** don't have cursors. +/// +/// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq)] +#[reflect(Debug, PartialEq, Default)] pub enum CursorGrabMode { /// The cursor can freely leave the window. + #[default] None, /// The cursor is confined to the window area. Confined, @@ -423,103 +541,32 @@ pub enum CursorGrabMode { Locked, } -/// Defines the way a window is displayed. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -#[reflect(Debug, PartialEq)] -pub enum WindowMode { - /// Creates a window that uses the given size. - Windowed, - /// Creates a borderless window that uses the full size of the screen. - BorderlessFullscreen, - /// Creates a fullscreen window that will render at desktop resolution. - /// - /// The app will use the closest supported size from the given size and scale it to fit the screen. - SizedFullscreen, - /// Creates a fullscreen window that uses the maximum supported size. - Fullscreen, -} - -impl Window { - /// Creates a new [`Window`]. - pub fn new( - id: WindowId, - window_descriptor: &WindowDescriptor, - physical_width: u32, - physical_height: u32, - scale_factor: f64, - position: Option, - raw_handle: Option, - ) -> Self { - Window { - id, - requested_width: window_descriptor.width, - requested_height: window_descriptor.height, - position, - physical_width, - physical_height, - resize_constraints: window_descriptor.resize_constraints, - scale_factor_override: window_descriptor.scale_factor_override, - backend_scale_factor: scale_factor, - title: window_descriptor.title.clone(), - present_mode: window_descriptor.present_mode, - resizable: window_descriptor.resizable, - decorations: window_descriptor.decorations, - cursor_visible: window_descriptor.cursor_visible, - cursor_grab_mode: window_descriptor.cursor_grab_mode, - cursor_icon: CursorIcon::Default, - hittest: true, - physical_cursor_position: None, - raw_handle, - focused: false, - mode: window_descriptor.mode, - canvas: window_descriptor.canvas.clone(), - fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, - command_queue: Vec::new(), - alpha_mode: window_descriptor.alpha_mode, - always_on_top: window_descriptor.always_on_top, +impl WindowResolution { + /// Creates a new [`WindowResolution`]. + pub fn new(logical_width: f32, logical_height: f32) -> Self { + Self { + physical_width: logical_width as u32, + physical_height: logical_height as u32, + ..Default::default() } } - /// Get the window's [`WindowId`]. - #[inline] - pub fn id(&self) -> WindowId { - self.id + + /// Builder method for adding a scale factor override to the resolution. + pub fn with_scale_factor_override(mut self, scale_factor_override: f64) -> Self { + self.scale_factor_override = Some(scale_factor_override); + self } - /// The current logical width of the window's client area. + /// The window's client area width in logical pixels. #[inline] pub fn width(&self) -> f32 { - (self.physical_width as f64 / self.scale_factor()) as f32 + (self.physical_width() as f64 / self.scale_factor()) as f32 } - /// The current logical height of the window's client area. + /// The window's client area width in logical pixels. #[inline] pub fn height(&self) -> f32 { - (self.physical_height as f64 / self.scale_factor()) as f32 - } - - /// The requested window client area width in logical pixels from window - /// creation or the last call to [`set_resolution`](Window::set_resolution). - /// - /// This may differ from the actual width depending on OS size limits and - /// the scaling factor for high DPI monitors. - #[inline] - pub fn requested_width(&self) -> f32 { - self.requested_width - } - - /// The requested window client area height in logical pixels from window - /// creation or the last call to [`set_resolution`](Window::set_resolution). - /// - /// This may differ from the actual width depending on OS size limits and - /// the scaling factor for high DPI monitors. - #[inline] - pub fn requested_height(&self) -> f32 { - self.requested_height + (self.physical_height() as f64 / self.scale_factor()) as f32 } /// The window's client area width in physical pixels. @@ -534,380 +581,197 @@ impl Window { self.physical_height } - /// The window's client resize constraint in logical pixels. - #[inline] - pub fn resize_constraints(&self) -> WindowResizeConstraints { - self.resize_constraints + /// The ratio of physical pixels to logical pixels + /// + /// `physical_pixels = logical_pixels * scale_factor` + pub fn scale_factor(&self) -> f64 { + self.scale_factor_override + .unwrap_or_else(|| self.base_scale_factor()) } - /// The window's client position in physical pixels. - #[inline] - pub fn position(&self) -> Option { - self.position - } - /// Set whether or not the window is maximized. + /// The window scale factor as reported by the window backend. + /// + /// This value is unaffected by [`WindowResolution::scale_factor_override`]. #[inline] - pub fn set_maximized(&mut self, maximized: bool) { - self.command_queue - .push(WindowCommand::SetMaximized { maximized }); + pub fn base_scale_factor(&self) -> f64 { + self.scale_factor } - /// Sets the window to minimized or back. + /// The scale factor set with [`WindowResolution::set_scale_factor_override`]. /// - /// # Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - Wayland: Un-minimize is unsupported. + /// This value may be different from the scale factor reported by the window backend. #[inline] - pub fn set_minimized(&mut self, minimized: bool) { - self.command_queue - .push(WindowCommand::SetMinimized { minimized }); + pub fn scale_factor_override(&self) -> Option { + self.scale_factor_override } - /// Sets the `position` of the window on the selected `monitor` in physical pixels. - /// - /// This automatically un-maximizes the window if it's maximized. - /// - /// # Platform-specific - /// - /// - iOS: Can only be called on the main thread. Sets the top left coordinates of the window in - /// the screen space coordinate system. - /// - Web: Sets the top-left coordinates relative to the viewport. - /// - Android / Wayland: Unsupported. + /// Set the window's logical resolution. #[inline] - pub fn set_position(&mut self, monitor: MonitorSelection, position: IVec2) { - self.command_queue.push(WindowCommand::SetPosition { - monitor_selection: monitor, - position, - }); + pub fn set(&mut self, width: f32, height: f32) { + self.set_physical_resolution( + (width as f64 * self.scale_factor()) as u32, + (height as f64 * self.scale_factor()) as u32, + ); } - /// Modifies the position of the window to be in the center of the current monitor + /// Set the window's physical resolution. /// - /// # Platform-specific - /// - iOS: Can only be called on the main thread. - /// - Web / Android / Wayland: Unsupported. + /// You probably don't want to call this directly unless you are dealing + /// with a window manager library. #[inline] - pub fn center_window(&mut self, monitor_selection: MonitorSelection) { - self.command_queue - .push(WindowCommand::Center(monitor_selection)); + pub fn set_physical_resolution(&mut self, width: u32, height: u32) { + self.physical_width = width; + self.physical_height = height; } - /// Modifies the minimum and maximum window bounds for resizing in logical pixels. + /// Set the window's scale factor, this may get overriden by the backend. #[inline] - pub fn set_resize_constraints(&mut self, resize_constraints: WindowResizeConstraints) { - self.command_queue - .push(WindowCommand::SetResizeConstraints { resize_constraints }); + pub fn set_scale_factor(&mut self, scale_factor: f64) { + self.scale_factor = scale_factor; } - /// Request the OS to resize the window such the client area matches the specified - /// width and height. - #[allow(clippy::float_cmp)] - pub fn set_resolution(&mut self, width: f32, height: f32) { - if self.requested_width == width && self.requested_height == height { - return; - } - - self.requested_width = width; - self.requested_height = height; - self.command_queue.push(WindowCommand::SetResolution { - logical_resolution: Vec2::new(self.requested_width, self.requested_height), - scale_factor: self.scale_factor(), - }); + /// Set the window's scale factor, this will be used over what the backend decides. + #[inline] + pub fn set_scale_factor_override(&mut self, scale_factor_override: Option) { + self.scale_factor_override = scale_factor_override; } +} - /// Override the os-reported scaling factor. - #[allow(clippy::float_cmp)] - pub fn set_scale_factor_override(&mut self, scale_factor: Option) { - if self.scale_factor_override == scale_factor { - return; - } - - self.scale_factor_override = scale_factor; - self.command_queue.push(WindowCommand::SetScaleFactor { - scale_factor: self.scale_factor(), - }); - self.command_queue.push(WindowCommand::SetResolution { - logical_resolution: Vec2::new(self.requested_width, self.requested_height), - scale_factor: self.scale_factor(), - }); +impl From<(I, I)> for WindowResolution +where + I: Into, +{ + fn from((width, height): (I, I)) -> WindowResolution { + WindowResolution::new(width.into(), height.into()) } +} - #[allow(missing_docs)] - #[inline] - pub fn update_scale_factor_from_backend(&mut self, scale_factor: f64) { - self.backend_scale_factor = scale_factor; +impl From<[I; 2]> for WindowResolution +where + I: Into, +{ + fn from([width, height]: [I; 2]) -> WindowResolution { + WindowResolution::new(width.into(), height.into()) } +} - #[allow(missing_docs)] - #[inline] - pub fn update_actual_size_from_backend(&mut self, physical_width: u32, physical_height: u32) { - self.physical_width = physical_width; - self.physical_height = physical_height; +impl From for WindowResolution { + fn from(res: bevy_math::Vec2) -> WindowResolution { + WindowResolution::new(res.x, res.y) } +} - #[allow(missing_docs)] - #[inline] - pub fn update_actual_position_from_backend(&mut self, position: IVec2) { - self.position = Some(position); +impl From for WindowResolution { + fn from(res: bevy_math::DVec2) -> WindowResolution { + WindowResolution::new(res.x as f32, res.y as f32) } +} - /// The ratio of physical pixels to logical pixels +/// The different states a window can be in. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] +pub struct WindowState { + /// Current maximization state of the window. + maximized: bool, + /// If this is true then next frame we will ask to minimize the window. + request_minimize: bool, + /// If this is true then next frame we will ask to maximize/un-maximize the window depending on `maximized`. + request_maximize: bool, +} + +impl WindowState { + /// Constructor method for starting a window as minimized. /// - /// `physical_pixels = logical_pixels * scale_factor` - pub fn scale_factor(&self) -> f64 { - self.scale_factor_override - .unwrap_or(self.backend_scale_factor) + /// Note: This may not work on every platform. + pub fn minimized() -> Self { + let mut state = Self::default(); + state.minimize(); + state } - /// The window scale factor as reported by the window backend. + /// Constructor method for starting a window as maximized. /// - /// This value is unaffected by [`scale_factor_override`](Window::scale_factor_override). - #[inline] - pub fn backend_scale_factor(&self) -> f64 { - self.backend_scale_factor + /// Note: This may not work on every platform. + pub fn maximized() -> Self { + let mut state = Self::default(); + state.set_maximize(true); + state } - /// The scale factor set with [`set_scale_factor_override`](Window::set_scale_factor_override). + + /// Setting this to true will attempt to maximize the window. /// - /// This value may be different from the scale factor reported by the window backend. - #[inline] - pub fn scale_factor_override(&self) -> Option { - self.scale_factor_override - } - /// Get the window's title. - #[inline] - pub fn title(&self) -> &str { - &self.title - } - /// Set the window's title. - pub fn set_title(&mut self, title: String) { - self.title = title.to_string(); - self.command_queue.push(WindowCommand::SetTitle { title }); + /// Setting it to false will attempt to un-maximize the window. + pub fn set_maximize(&mut self, maximize: bool) { + self.maximized = maximize; + self.request_maximize = true; } - #[inline] - #[doc(alias = "vsync")] - /// Get the window's [`PresentMode`]. - pub fn present_mode(&self) -> PresentMode { - self.present_mode + /// Calling this will attempt to minimize the window. + pub fn minimize(&mut self) { + self.request_minimize = true; } - #[inline] - /// Get the window's [`CompositeAlphaMode`]. - pub fn alpha_mode(&self) -> CompositeAlphaMode { - self.alpha_mode + /// Returns true if [`WindowState`] is Maximized. + pub fn is_maximized(&self) -> bool { + self.maximized } - #[inline] - #[doc(alias = "set_vsync")] - /// Set the window's [`PresentMode`]. - pub fn set_present_mode(&mut self, present_mode: PresentMode) { - self.present_mode = present_mode; - self.command_queue - .push(WindowCommand::SetPresentMode { present_mode }); - } - /// Get whether or not the window is resizable. - #[inline] - pub fn resizable(&self) -> bool { - self.resizable - } - /// Set whether or not the window is resizable. - pub fn set_resizable(&mut self, resizable: bool) { - self.resizable = resizable; - self.command_queue - .push(WindowCommand::SetResizable { resizable }); - } - /// Get whether or not decorations are enabled. - /// - /// (Decorations are the minimize, maximize, and close buttons on desktop apps) - /// - /// ## Platform-specific - /// - /// **`iOS`**, **`Android`**, and the **`Web`** do not have decorations. - #[inline] - pub fn decorations(&self) -> bool { - self.decorations - } - /// Set whether or not decorations are enabled. - /// - /// (Decorations are the minimize, maximize, and close buttons on desktop apps) - /// - /// ## Platform-specific - /// - /// **`iOS`**, **`Android`**, and the **`Web`** do not have decorations. - pub fn set_decorations(&mut self, decorations: bool) { - self.decorations = decorations; - self.command_queue - .push(WindowCommand::SetDecorations { decorations }); - } - /// Get whether or how the cursor is grabbed. - /// - /// ## Platform-specific - /// - /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] - /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] - /// - **`iOS/Android`** don't have cursors. - /// - /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, it's possible the value returned here is not the same as the one actually sent to winit. - #[inline] - pub fn cursor_grab_mode(&self) -> CursorGrabMode { - self.cursor_grab_mode - } - /// Set whether and how the cursor is grabbed. - /// - /// This doesn't hide the cursor. For that, use [`set_cursor_visibility`](Window::set_cursor_visibility) - /// - /// ## Platform-specific - /// - /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] - /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] - /// - **`iOS/Android`** don't have cursors. - /// - /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. - pub fn set_cursor_grab_mode(&mut self, grab_mode: CursorGrabMode) { - self.cursor_grab_mode = grab_mode; - self.command_queue - .push(WindowCommand::SetCursorGrabMode { grab_mode }); - } - /// Get whether or not the cursor is visible. - /// - /// ## Platform-specific - /// - /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window. To stop the cursor from leaving the window, use [`set_cursor_grab_mode`](Window::set_cursor_grab_mode). - /// - **`macOS`**: The cursor is hidden only when the window is focused. - /// - **`iOS`** and **`Android`** do not have cursors - #[inline] - pub fn cursor_visible(&self) -> bool { - self.cursor_visible - } - /// Set whether or not the cursor is visible. - /// - /// ## Platform-specific - /// - /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window. To stop the cursor from leaving the window, use [`set_cursor_grab_mode`](Window::set_cursor_grab_mode). - /// - **`macOS`**: The cursor is hidden only when the window is focused. - /// - **`iOS`** and **`Android`** do not have cursors - pub fn set_cursor_visibility(&mut self, visible_mode: bool) { - self.cursor_visible = visible_mode; - self.command_queue.push(WindowCommand::SetCursorVisibility { - visible: visible_mode, - }); - } - /// Get the current [`CursorIcon`] - #[inline] - pub fn cursor_icon(&self) -> CursorIcon { - self.cursor_icon - } - /// Set the [`CursorIcon`] - pub fn set_cursor_icon(&mut self, icon: CursorIcon) { - self.command_queue - .push(WindowCommand::SetCursorIcon { icon }); + /// Whether we should try to minimize this frame. + pub fn requesting_minimize(&self) -> bool { + self.request_minimize } - /// The current mouse position, in physical pixels. - #[inline] - pub fn physical_cursor_position(&self) -> Option { - self.physical_cursor_position + /// Whether we should try to (un)maximize this frame. + pub fn requesting_maximize(&self) -> bool { + self.request_maximize } - /// The current mouse position, in logical pixels, taking into account the screen scale factor. - #[inline] - #[doc(alias = "mouse position")] - pub fn cursor_position(&self) -> Option { - self.physical_cursor_position - .map(|p| (p / self.scale_factor()).as_vec2()) - } - /// Set the cursor's position - pub fn set_cursor_position(&mut self, position: Vec2) { - self.command_queue - .push(WindowCommand::SetCursorPosition { position }); + /// Clear requests for minimizing/maximizing. + pub fn clear_requests(&mut self) { + self.request_maximize = false; + self.request_minimize = false; } - /// Modifies whether the window catches cursor events. + + /// Set if the window is actually maximized or not. /// - /// If true, the window will catch the cursor events. - /// If false, events are passed through the window such that any other window behind it receives them. By default hittest is enabled. - pub fn set_cursor_hittest(&mut self, hittest: bool) { - self.hittest = hittest; - self.command_queue - .push(WindowCommand::SetCursorHitTest { hittest }); - } - /// Get whether or not the hittest is active. - #[inline] - pub fn hittest(&self) -> bool { - self.hittest - } - #[allow(missing_docs)] - #[inline] - pub fn update_focused_status_from_backend(&mut self, focused: bool) { - self.focused = focused; + /// You probably don't want this as this is usually done by the + /// windowing backend. Instead use [`WindowState::set_maximize`]. + pub fn set_maximize_by_backend(&mut self, maximized: bool) { + self.maximized = maximized; } +} - #[allow(missing_docs)] - #[inline] - pub fn update_cursor_physical_position_from_backend(&mut self, cursor_position: Option) { - self.physical_cursor_position = cursor_position; - } - /// Get the window's [`WindowMode`] - #[inline] - pub fn mode(&self) -> WindowMode { - self.mode - } - /// Set the window's [`WindowMode`] - pub fn set_mode(&mut self, mode: WindowMode) { - self.mode = mode; - self.command_queue.push(WindowCommand::SetWindowMode { - mode, - resolution: UVec2::new(self.physical_width, self.physical_height), - }); - } - /// Get whether or not the window is always on top. - #[inline] - pub fn always_on_top(&self) -> bool { - self.always_on_top - } +/// Defines how this window should behave in relation to web-canvas elements. +#[derive(Default, Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] +pub struct WindowCanvas { + canvas: Option, + fit_canvas_to_parent: bool, +} - /// Set whether of not the window is always on top. - pub fn set_always_on_top(&mut self, always_on_top: bool) { - self.always_on_top = always_on_top; - self.command_queue - .push(WindowCommand::SetAlwaysOnTop { always_on_top }); - } - /// 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 + '_ { - self.command_queue.drain(..) - } - /// Get whether or not the window has focus. - /// - /// A window loses focus when the user switches to another window, and regains focus when the user uses the window again - #[inline] - pub fn is_focused(&self) -> bool { - self.focused - } - /// Get the [`RawHandleWrapper`] corresponding to this window if set. - /// - /// During normal use, this can be safely unwrapped; the value should only be [`None`] when synthetically constructed for tests. - pub fn raw_handle(&self) -> Option { - self.raw_handle.as_ref().cloned() +impl WindowCanvas { + /// Creates a new [`WindowCanvas`]. + pub fn new(canvas: Option, fit_canvas_to_parent: bool) -> Self { + Self { + canvas, + fit_canvas_to_parent, + } } /// The "html canvas" element selector. /// /// If set, this selector will be used to find a matching html canvas element, - /// rather than creating a new one. + /// rather than creating a new one. /// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). /// /// This value has no effect on non-web platforms. @@ -929,29 +793,6 @@ impl Window { } } -/// Defines where window should be placed at on creation. -#[derive(Debug, Clone, Copy, PartialEq, Reflect, FromReflect)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -#[reflect(Debug, PartialEq)] -pub enum WindowPosition { - /// The position will be set by the window manager. - Automatic, - /// Center the window on the monitor. - /// - /// The monitor to center the window on can be selected with the `monitor` field in `WindowDescriptor`. - Centered, - /// The window's top-left corner will be placed at the specified position in pixels. - /// - /// (0,0) represents top-left corner of the monitor. - /// - /// The monitor to position the window on can be selected with the `monitor` field in `WindowDescriptor`. - At(Vec2), -} - /// Defines which monitor to use. #[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] #[cfg_attr( @@ -970,130 +811,3 @@ pub enum MonitorSelection { /// Uses monitor with the specified index. Index(usize), } - -/// Describes the information needed for creating a window. -/// -/// This should be set up before adding the [`WindowPlugin`](crate::WindowPlugin). -/// Most of these settings can also later be configured through the [`Window`](crate::Window) resource. -/// -/// See [`examples/window/window_settings.rs`] for usage. -/// -/// [`examples/window/window_settings.rs`]: https://github.com/bevyengine/bevy/blob/latest/examples/window/window_settings.rs -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -#[reflect(Debug, PartialEq, Default)] -pub struct WindowDescriptor { - /// The requested logical width of the window's client area. - /// - /// May vary from the physical width due to different pixel density on different monitors. - pub width: f32, - /// The requested logical height of the window's client area. - /// - /// May vary from the physical height due to different pixel density on different monitors. - pub height: f32, - /// The position on the screen that the window will be placed at. - /// - /// The monitor to place the window on can be selected with the `monitor` field. - /// - /// Ignored if `mode` is set to something other than [`WindowMode::Windowed`] - /// - /// `WindowPosition::Automatic` will be overridden with `WindowPosition::At(Vec2::ZERO)` if a specific monitor is selected. - pub position: WindowPosition, - /// The monitor to place the window on. - pub monitor: MonitorSelection, - /// Sets minimum and maximum resize limits. - pub resize_constraints: WindowResizeConstraints, - /// Overrides the window's ratio of physical pixels to logical pixels. - /// - /// If there are some scaling problems on X11 try to set this option to `Some(1.0)`. - pub scale_factor_override: Option, - /// Sets the title that displays on the window top bar, on the system task bar and other OS specific places. - /// - /// ## Platform-specific - /// - Web: Unsupported. - pub title: String, - /// Controls when a frame is presented to the screen. - #[doc(alias = "vsync")] - /// The window's [`PresentMode`]. - /// - /// Used to select whether or not VSync is used - pub present_mode: PresentMode, - /// Sets whether the window is resizable. - /// - /// ## Platform-specific - /// - iOS / Android / Web: Unsupported. - pub resizable: bool, - /// Sets whether the window should have borders and bars. - pub decorations: bool, - /// Sets whether the cursor is visible when the window has focus. - pub cursor_visible: bool, - /// Sets whether and how the window grabs the cursor. - pub cursor_grab_mode: CursorGrabMode, - /// Sets whether or not the window listens for 'hits' of mouse activity over _this_ window. - pub hittest: bool, - /// Sets the [`WindowMode`](crate::WindowMode). - /// - /// The monitor to go fullscreen on can be selected with the `monitor` field. - pub mode: WindowMode, - /// Sets whether the background of the window should be transparent. - /// - /// ## Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - macOS: Not working as expected. See [Bevy #6330](https://github.com/bevyengine/bevy/issues/6330). - /// - Linux (Wayland): Not working as expected. See [Bevy #5779](https://github.com/bevyengine/bevy/issues/5779). - pub transparent: bool, - /// The "html canvas" element selector. - /// - /// If set, this selector will be used to find a matching html canvas element, - /// rather than creating a new one. - /// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). - /// - /// This value has no effect on non-web platforms. - pub canvas: Option, - /// Whether or not to fit the canvas element's size to its parent element's size. - /// - /// **Warning**: this will not behave as expected for parents that set their size according to the size of their - /// children. This creates a "feedback loop" that will result in the canvas growing on each resize. When using this - /// feature, ensure the parent's size is not affected by its children. - /// - /// This value has no effect on non-web platforms. - pub fit_canvas_to_parent: bool, - /// Specifies how the alpha channel of the textures should be handled during compositing. - pub alpha_mode: CompositeAlphaMode, - /// Sets the window to always be on top of other windows. - /// - /// ## Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - Linux (Wayland): Unsupported. - pub always_on_top: bool, -} - -impl Default for WindowDescriptor { - fn default() -> Self { - WindowDescriptor { - title: "app".to_string(), - width: 1280., - height: 720., - position: WindowPosition::Automatic, - monitor: MonitorSelection::Current, - resize_constraints: WindowResizeConstraints::default(), - scale_factor_override: None, - present_mode: PresentMode::Fifo, - resizable: true, - decorations: true, - cursor_grab_mode: CursorGrabMode::None, - cursor_visible: true, - hittest: true, - mode: WindowMode::Windowed, - transparent: false, - canvas: None, - fit_canvas_to_parent: false, - alpha_mode: CompositeAlphaMode::Auto, - always_on_top: false, - } - } -} diff --git a/crates/bevy_winit/src/converters.rs b/crates/bevy_winit/src/converters.rs index 6f2f23e785761..2e6d0cf7ce3dc 100644 --- a/crates/bevy_winit/src/converters.rs +++ b/crates/bevy_winit/src/converters.rs @@ -33,7 +33,7 @@ pub fn convert_mouse_button(mouse_button: winit::event::MouseButton) -> MouseBut pub fn convert_touch_input( touch_input: winit::event::Touch, - location: winit::dpi::LogicalPosition, + location: winit::dpi::LogicalPosition, ) -> TouchInput { TouchInput { phase: match touch_input.phase { @@ -42,7 +42,7 @@ pub fn convert_touch_input( winit::event::TouchPhase::Ended => TouchPhase::Ended, winit::event::TouchPhase::Cancelled => TouchPhase::Cancelled, }, - position: Vec2::new(location.x, location.y), + position: Vec2::new(location.x as f32, location.y as f32), force: touch_input.force.map(|f| match f { winit::event::Force::Calibrated { force, diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 749d256882500..2b8dcc509ecef 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,266 +1,111 @@ mod converters; +mod system; #[cfg(target_arch = "wasm32")] mod web_resize; mod winit_config; mod winit_windows; -use winit::window::CursorGrabMode; +use bevy_ecs::system::{SystemParam, SystemState}; +use system::{changed_window, create_window, despawn_window}; + pub use winit_config::*; pub use winit_windows::*; use bevy_app::{App, AppExit, CoreStage, Plugin}; +use bevy_ecs::event::{Events, ManualEventReader}; use bevy_ecs::prelude::*; -use bevy_ecs::{ - event::{Events, ManualEventReader}, - world::World, +use bevy_input::{ + keyboard::KeyboardInput, + mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + touch::TouchInput, }; -use bevy_input::mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}; -use bevy_math::{ivec2, DVec2, UVec2, Vec2}; +use bevy_math::{ivec2, DVec2, Vec2}; use bevy_utils::{ - tracing::{error, info, trace, warn}, + tracing::{trace, warn}, Instant, }; use bevy_window::{ - CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows, - ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, - WindowClosed, WindowCreated, WindowFocused, WindowMoved, WindowResized, - WindowScaleFactorChanged, Windows, + CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows, ReceivedCharacter, + RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, + WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, }; use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, event::{self, DeviceEvent, Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, }; +#[cfg(target_arch = "wasm32")] +use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin}; + #[derive(Default)] pub struct WinitPlugin; impl Plugin for WinitPlugin { fn build(&self, app: &mut App) { + let event_loop = EventLoop::new(); + app.insert_non_send_resource(event_loop); + app.init_non_send_resource::() .init_resource::() .set_runner(winit_runner) - .add_system_to_stage(CoreStage::PostUpdate, change_window.label(ModifiesWindows)); - #[cfg(target_arch = "wasm32")] - app.add_plugin(web_resize::CanvasParentResizePlugin); - let event_loop = EventLoop::new(); - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] - let mut create_window_reader = WinitCreateWindowReader::default(); - #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] - let create_window_reader = WinitCreateWindowReader::default(); - // Note that we create a window here "early" because WASM/WebGL requires the window to exist prior to initializing - // the renderer. - // And for ios and macos, we should not create window early, all ui related code should be executed inside - // UIApplicationMain/NSApplicationMain. - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] - handle_create_window_events(&mut app.world, &event_loop, &mut create_window_reader.0); - app.insert_resource(create_window_reader) - .insert_non_send_resource(event_loop); - } -} + .add_system_set_to_stage( + CoreStage::PostUpdate, + SystemSet::new() + .label(ModifiesWindows) + .with_system(changed_window) + .with_system(despawn_window), + ); -fn change_window( - mut winit_windows: NonSendMut, - mut windows: ResMut, - mut window_dpi_changed_events: EventWriter, - mut window_close_events: EventWriter, -) { - let mut removed_windows = vec![]; - for bevy_window in windows.iter_mut() { - let id = bevy_window.id(); - for command in bevy_window.drain_commands() { - match command { - bevy_window::WindowCommand::SetWindowMode { - mode, - resolution: - UVec2 { - x: width, - y: height, - }, - } => { - let window = winit_windows.get_window(id).unwrap(); - match mode { - bevy_window::WindowMode::BorderlessFullscreen => { - window - .set_fullscreen(Some(winit::window::Fullscreen::Borderless(None))); - } - bevy_window::WindowMode::Fullscreen => { - window.set_fullscreen(Some(winit::window::Fullscreen::Exclusive( - get_best_videomode(&window.current_monitor().unwrap()), - ))); - } - bevy_window::WindowMode::SizedFullscreen => window.set_fullscreen(Some( - winit::window::Fullscreen::Exclusive(get_fitting_videomode( - &window.current_monitor().unwrap(), - width, - height, - )), - )), - bevy_window::WindowMode::Windowed => window.set_fullscreen(None), - } - } - bevy_window::WindowCommand::SetTitle { title } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_title(&title); - } - bevy_window::WindowCommand::SetScaleFactor { scale_factor } => { - window_dpi_changed_events.send(WindowScaleFactorChanged { id, scale_factor }); - } - bevy_window::WindowCommand::SetResolution { - logical_resolution: - Vec2 { - x: width, - y: height, - }, - scale_factor, - } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_inner_size( - winit::dpi::LogicalSize::new(width, height) - .to_physical::(scale_factor), - ); - } - bevy_window::WindowCommand::SetPresentMode { .. } => (), - bevy_window::WindowCommand::SetResizable { resizable } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_resizable(resizable); - } - bevy_window::WindowCommand::SetDecorations { decorations } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_decorations(decorations); - } - bevy_window::WindowCommand::SetCursorIcon { icon } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_cursor_icon(converters::convert_cursor_icon(icon)); - } - bevy_window::WindowCommand::SetCursorGrabMode { grab_mode } => { - let window = winit_windows.get_window(id).unwrap(); - match grab_mode { - bevy_window::CursorGrabMode::None => { - window.set_cursor_grab(CursorGrabMode::None) - } - bevy_window::CursorGrabMode::Confined => window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)), - bevy_window::CursorGrabMode::Locked => window - .set_cursor_grab(CursorGrabMode::Locked) - .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Confined)), - } - .unwrap_or_else(|e| error!("Unable to un/grab cursor: {}", e)); - } - bevy_window::WindowCommand::SetCursorVisibility { visible } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_cursor_visible(visible); - } - bevy_window::WindowCommand::SetCursorPosition { position } => { - let window = winit_windows.get_window(id).unwrap(); - let inner_size = window.inner_size().to_logical::(window.scale_factor()); - window - .set_cursor_position(LogicalPosition::new( - position.x, - inner_size.height - position.y, - )) - .unwrap_or_else(|e| error!("Unable to set cursor position: {}", e)); - } - bevy_window::WindowCommand::SetMaximized { maximized } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_maximized(maximized); - } - bevy_window::WindowCommand::SetMinimized { minimized } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_minimized(minimized); - } - bevy_window::WindowCommand::SetPosition { - monitor_selection, - position, - } => { - let window = winit_windows.get_window(id).unwrap(); - - use bevy_window::MonitorSelection::*; - let maybe_monitor = match monitor_selection { - Current => window.current_monitor(), - Primary => window.primary_monitor(), - Index(i) => window.available_monitors().nth(i), - }; - if let Some(monitor) = maybe_monitor { - let monitor_position = DVec2::from(<(_, _)>::from(monitor.position())); - let position = monitor_position + position.as_dvec2(); - - window.set_outer_position(LogicalPosition::new(position.x, position.y)); - } else { - warn!("Couldn't get monitor selected with: {monitor_selection:?}"); - } - } - bevy_window::WindowCommand::Center(monitor_selection) => { - let window = winit_windows.get_window(id).unwrap(); - - use bevy_window::MonitorSelection::*; - let maybe_monitor = match monitor_selection { - Current => window.current_monitor(), - Primary => window.primary_monitor(), - Index(i) => window.available_monitors().nth(i), - }; - - if let Some(monitor) = maybe_monitor { - let monitor_size = monitor.size(); - let monitor_position = monitor.position().cast::(); + #[cfg(target_arch = "wasm32")] + app.add_plugin(CanvasParentResizePlugin); - let window_size = window.outer_size(); + #[cfg(not(target_arch = "wasm32"))] + let mut create_window_system_state: SystemState<( + Commands, + NonSendMut>, + Query<(Entity, &Window)>, + EventWriter, + NonSendMut, + )> = SystemState::from_world(&mut app.world); - window.set_outer_position(PhysicalPosition { - x: monitor_size.width.saturating_sub(window_size.width) as f64 / 2. - + monitor_position.x, - y: monitor_size.height.saturating_sub(window_size.height) as f64 / 2. - + monitor_position.y, - }); - } else { - warn!("Couldn't get monitor selected with: {monitor_selection:?}"); - } - } - bevy_window::WindowCommand::SetResizeConstraints { resize_constraints } => { - let window = winit_windows.get_window(id).unwrap(); - let constraints = resize_constraints.check_constraints(); - let min_inner_size = LogicalSize { - width: constraints.min_width, - height: constraints.min_height, - }; - let max_inner_size = LogicalSize { - width: constraints.max_width, - height: constraints.max_height, - }; + #[cfg(target_arch = "wasm32")] + let mut create_window_system_state: SystemState<( + Commands, + NonSendMut>, + Query<(Entity, &Window)>, + EventWriter, + NonSendMut, + ResMut, + )> = SystemState::from_world(&mut app.world); - window.set_min_inner_size(Some(min_inner_size)); - if constraints.max_width.is_finite() && constraints.max_height.is_finite() { - window.set_max_inner_size(Some(max_inner_size)); - } - } - bevy_window::WindowCommand::SetAlwaysOnTop { always_on_top } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_always_on_top(always_on_top); - } - bevy_window::WindowCommand::SetCursorHitTest { hittest } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_cursor_hittest(hittest).unwrap(); - } - 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 }); + // And for ios and macos, we should not create window early, all ui related code should be executed inside + // UIApplicationMain/NSApplicationMain. + //#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] + { + #[cfg(not(target_arch = "wasm32"))] + let (commands, event_loop, new_windows, event_writer, winit_windows) = + create_window_system_state.get_mut(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let (commands, event_loop, new_windows, event_writer, winit_windows, event_channel) = + create_window_system_state.get_mut(&mut app.world); + + // Here we need to create a winit-window and give it a WindowHandle which the renderer can use. + // It needs to be spawned before the start of the startup-stage, so we cannot use a regular system. + // Instead we need to create the window and spawn it using direct world access + create_window( + commands, + &event_loop, + new_windows.iter(), + event_writer, + winit_windows, + #[cfg(target_arch = "wasm32")] + event_channel, + ); } + + create_window_system_state.apply(&mut app.world); } } @@ -307,8 +152,30 @@ where panic!("Run return is not supported on this platform!") } -pub fn winit_runner(app: App) { - winit_runner_with(app); +#[derive(SystemParam)] +struct WindowEvents<'w> { + window_resized: EventWriter<'w, WindowResized>, + window_close_requested: EventWriter<'w, WindowCloseRequested>, + window_scale_factor_changed: EventWriter<'w, WindowScaleFactorChanged>, + window_backend_scale_factor_changed: EventWriter<'w, WindowBackendScaleFactorChanged>, + window_focused: EventWriter<'w, WindowFocused>, + window_moved: EventWriter<'w, WindowMoved>, +} + +#[derive(SystemParam)] +struct InputEvents<'w> { + keyboard_input: EventWriter<'w, KeyboardInput>, + character_input: EventWriter<'w, ReceivedCharacter>, + mouse_button_input: EventWriter<'w, MouseButtonInput>, + mouse_wheel_input: EventWriter<'w, MouseWheel>, + touch_input: EventWriter<'w, TouchInput>, +} + +#[derive(SystemParam)] +struct CursorEvents<'w> { + cursor_moved: EventWriter<'w, CursorMoved>, + cursor_entered: EventWriter<'w, CursorEntered>, + cursor_left: EventWriter<'w, CursorLeft>, } // #[cfg(any( @@ -347,19 +214,13 @@ impl Default for WinitPersistentState { } } -#[derive(Default, Resource)] -struct WinitCreateWindowReader(ManualEventReader); - -pub fn winit_runner_with(mut app: App) { +pub fn winit_runner(mut app: App) { + // We remove this so that we have ownership over it. let mut event_loop = app .world .remove_non_send_resource::>() .unwrap(); - let mut create_window_event_reader = app - .world - .remove_resource::() - .unwrap() - .0; + let mut app_exit_event_reader = ManualEventReader::::default(); let mut redraw_event_reader = ManualEventReader::::default(); let mut winit_state = WinitPersistentState::default(); @@ -370,23 +231,80 @@ pub fn winit_runner_with(mut app: App) { trace!("Entering winit event loop"); + let mut focused_window_state: SystemState<(Res, Query<&Window>)> = + SystemState::from_world(&mut app.world); + + #[cfg(not(target_arch = "wasm32"))] + let mut create_window_system_state: SystemState<( + Commands, + Query<(Entity, &Window), Added>, + EventWriter, + NonSendMut, + )> = SystemState::from_world(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let mut create_window_system_state: SystemState<( + Commands, + Query<(Entity, &Window), Added>, + EventWriter, + NonSendMut, + ResMut, + )> = SystemState::from_world(&mut app.world); + let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { #[cfg(feature = "trace")] let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); + + if let Some(app_exit_events) = app.world.get_resource::>() { + if app_exit_event_reader.iter(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + return; + } + } + + { + #[cfg(not(target_arch = "wasm32"))] + let (commands, new_windows, created_window_writer, winit_windows) = + create_window_system_state.get_mut(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let ( + commands, + new_windows, + created_window_writer, + winit_windows, + canvas_parent_resize_channel, + ) = create_window_system_state.get_mut(&mut app.world); + + // Responsible for creating new windows + create_window( + commands, + event_loop, + new_windows.iter(), + created_window_writer, + winit_windows, + #[cfg(target_arch = "wasm32")] + canvas_parent_resize_channel, + ); + + create_window_system_state.apply(&mut app.world); + } + match event { event::Event::NewEvents(start) => { - let winit_config = app.world.resource::(); - let windows = app.world.resource::(); - let focused = windows.iter().any(|w| w.is_focused()); + let (winit_config, window_focused_query) = focused_window_state.get(&app.world); + + let app_focused = window_focused_query.iter().any(|window| window.focused); + // Check if either the `WaitUntil` timeout was triggered by winit, or that same // amount of time has elapsed since the last app update. This manual check is needed // because we don't know if the criteria for an app update were met until the end of // the frame. let auto_timeout_reached = matches!(start, StartCause::ResumeTimeReached { .. }); let now = Instant::now(); - let manual_timeout_reached = match winit_config.update_mode(focused) { + let manual_timeout_reached = match winit_config.update_mode(app_focused) { UpdateMode::Continuous => false, UpdateMode::Reactive { max_wait } | UpdateMode::ReactiveLowPower { max_wait } => { @@ -402,81 +320,118 @@ pub fn winit_runner_with(mut app: App) { window_id: winit_window_id, .. } => { - let world = app.world.cell(); - let winit_windows = world.non_send_resource_mut::(); - let mut windows = world.resource_mut::(); - let window_id = - if let Some(window_id) = winit_windows.get_window_id(winit_window_id) { - window_id + // Fetch and prepare details from the world + let mut system_state: SystemState<( + NonSend, + Query<&mut Window>, + WindowEvents, + InputEvents, + CursorEvents, + EventWriter, + )> = SystemState::new(&mut app.world); + let ( + winit_windows, + mut window_query, + mut window_events, + mut input_events, + mut cursor_events, + mut file_drag_and_drop_events, + ) = system_state.get_mut(&mut app.world); + + // Entity of this window + let window_entity = + if let Some(entity) = winit_windows.get_window_entity(winit_window_id) { + entity } else { warn!( - "Skipped event for unknown winit Window Id {:?}", - winit_window_id + "Skipped event {:?} for unknown winit Window Id {:?}", + event, winit_window_id ); return; }; - let Some(window) = windows.get_mut(window_id) else { - // If we're here, this window was previously opened - info!("Skipped event for closed window: {:?}", window_id); + let mut window = if let Ok(window) = window_query.get_mut(window_entity) { + window + } else { + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event + ); return; }; + winit_state.low_power_event = true; match event { WindowEvent::Resized(size) => { - window.update_actual_size_from_backend(size.width, size.height); - world.send_event(WindowResized { - id: window_id, - width: window.width(), - height: window.height(), + window + .resolution + .set_physical_resolution(size.width, size.height); + + window_events.window_resized.send(WindowResized { + window: window_entity, + width: window.resolution.width(), + height: window.resolution.height(), }); } WindowEvent::CloseRequested => { - world.send_event(WindowCloseRequested { id: window_id }); + window_events + .window_close_requested + .send(WindowCloseRequested { + window: window_entity, + }); } WindowEvent::KeyboardInput { ref input, .. } => { - world.send_event(converters::convert_keyboard_input(input)); + input_events + .keyboard_input + .send(converters::convert_keyboard_input(input)); } WindowEvent::CursorMoved { position, .. } => { - let winit_window = winit_windows.get_window(window_id).unwrap(); - let inner_size = winit_window.inner_size(); - - // move origin to bottom left - let y_position = inner_size.height as f64 - position.y; + let physical_position = DVec2::new( + position.x, + // Flip the coordinate space from winit's context to our context. + window.resolution.physical_height() as f64 - position.y, + ); - let physical_position = DVec2::new(position.x, y_position); - window - .update_cursor_physical_position_from_backend(Some(physical_position)); + window.cursor_position = Some(physical_position); - world.send_event(CursorMoved { - id: window_id, - position: (physical_position / window.scale_factor()).as_vec2(), + cursor_events.cursor_moved.send(CursorMoved { + window: window_entity, + position: (physical_position / window.resolution.scale_factor()) + .as_vec2(), }); } WindowEvent::CursorEntered { .. } => { - world.send_event(CursorEntered { id: window_id }); + cursor_events.cursor_entered.send(CursorEntered { + window: window_entity, + }); } WindowEvent::CursorLeft { .. } => { - window.update_cursor_physical_position_from_backend(None); - world.send_event(CursorLeft { id: window_id }); + // Component + if let Ok(mut window) = window_query.get_mut(window_entity) { + window.cursor_position = None; + } + + cursor_events.cursor_left.send(CursorLeft { + window: window_entity, + }); } WindowEvent::MouseInput { state, button, .. } => { - world.send_event(MouseButtonInput { + input_events.mouse_button_input.send(MouseButtonInput { button: converters::convert_mouse_button(button), state: converters::convert_element_state(state), }); } WindowEvent::MouseWheel { delta, .. } => match delta { event::MouseScrollDelta::LineDelta(x, y) => { - world.send_event(MouseWheel { + input_events.mouse_wheel_input.send(MouseWheel { unit: MouseScrollUnit::Line, x, y, }); } event::MouseScrollDelta::PixelDelta(p) => { - world.send_event(MouseWheel { + input_events.mouse_wheel_input.send(MouseWheel { unit: MouseScrollUnit::Pixel, x: p.x as f32, y: p.y as f32, @@ -484,12 +439,23 @@ pub fn winit_runner_with(mut app: App) { } }, WindowEvent::Touch(touch) => { - let location = touch.location.to_logical(window.scale_factor()); - world.send_event(converters::convert_touch_input(touch, location)); + let mut location = + touch.location.to_logical(window.resolution.scale_factor()); + + // On a mobile window, the start is from the top while on PC/Linux/OSX from + // bottom + if cfg!(target_os = "android") || cfg!(target_os = "ios") { + location.y = window.resolution.height() as f64 - location.y; + } + + // Event + input_events + .touch_input + .send(converters::convert_touch_input(touch, location)); } WindowEvent::ReceivedCharacter(c) => { - world.send_event(ReceivedCharacter { - id: window_id, + input_events.character_input.send(ReceivedCharacter { + window: window_entity, char: c, }); } @@ -497,73 +463,86 @@ pub fn winit_runner_with(mut app: App) { scale_factor, new_inner_size, } => { - world.send_event(WindowBackendScaleFactorChanged { - id: window_id, - scale_factor, - }); - let prior_factor = window.scale_factor(); - window.update_scale_factor_from_backend(scale_factor); - let new_factor = window.scale_factor(); - if let Some(forced_factor) = window.scale_factor_override() { + window_events.window_backend_scale_factor_changed.send( + WindowBackendScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + + let prior_factor = window.resolution.scale_factor(); + window.resolution.set_scale_factor(scale_factor); + let new_factor = window.resolution.scale_factor(); + + if let Some(forced_factor) = window.resolution.scale_factor_override() { // If there is a scale factor override, then force that to be used // Otherwise, use the OS suggested size // We have already told the OS about our resize constraints, so // the new_inner_size should take those into account *new_inner_size = winit::dpi::LogicalSize::new( - window.requested_width(), - window.requested_height(), + window.resolution.width(), + window.resolution.height(), ) .to_physical::(forced_factor); + // TODO: Should this not trigger a WindowsScaleFactorChanged? } else if approx::relative_ne!(new_factor, prior_factor) { - world.send_event(WindowScaleFactorChanged { - id: window_id, - scale_factor, - }); + // Trigger a change event if they are approximately different + window_events.window_scale_factor_changed.send( + WindowScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); } - let new_logical_width = new_inner_size.width as f64 / new_factor; - let new_logical_height = new_inner_size.height as f64 / new_factor; - if approx::relative_ne!(window.width() as f64, new_logical_width) - || approx::relative_ne!(window.height() as f64, new_logical_height) + let new_logical_width = (new_inner_size.width as f64 / new_factor) as f32; + let new_logical_height = (new_inner_size.height as f64 / new_factor) as f32; + if approx::relative_ne!(window.resolution.width(), new_logical_width) + || approx::relative_ne!(window.resolution.height(), new_logical_height) { - world.send_event(WindowResized { - id: window_id, - width: new_logical_width as f32, - height: new_logical_height as f32, + window_events.window_resized.send(WindowResized { + window: window_entity, + width: new_logical_width, + height: new_logical_height, }); } - window.update_actual_size_from_backend( - new_inner_size.width, - new_inner_size.height, - ); + window + .resolution + .set_physical_resolution(new_inner_size.width, new_inner_size.height); } WindowEvent::Focused(focused) => { - window.update_focused_status_from_backend(focused); - world.send_event(WindowFocused { - id: window_id, + // Component + window.focused = focused; + + window_events.window_focused.send(WindowFocused { + window: window_entity, focused, }); } WindowEvent::DroppedFile(path_buf) => { - world.send_event(FileDragAndDrop::DroppedFile { - id: window_id, + file_drag_and_drop_events.send(FileDragAndDrop::DroppedFile { + window: window_entity, path_buf, }); } WindowEvent::HoveredFile(path_buf) => { - world.send_event(FileDragAndDrop::HoveredFile { - id: window_id, + file_drag_and_drop_events.send(FileDragAndDrop::HoveredFile { + window: window_entity, path_buf, }); } WindowEvent::HoveredFileCancelled => { - world.send_event(FileDragAndDrop::HoveredFileCancelled { id: window_id }); + file_drag_and_drop_events.send(FileDragAndDrop::HoveredFileCancelled { + window: window_entity, + }); } WindowEvent::Moved(position) => { let position = ivec2(position.x, position.y); - window.update_actual_position_from_backend(position); - world.send_event(WindowMoved { - id: window_id, + + window.position.set(position); + + window_events.window_moved.send(WindowMoved { + entity: window_entity, position, }); } @@ -574,8 +553,12 @@ pub fn winit_runner_with(mut app: App) { event: DeviceEvent::MouseMotion { delta: (x, y) }, .. } => { - app.world.send_event(MouseMotion { - delta: DVec2 { x, y }.as_vec2(), + let mut system_state: SystemState> = + SystemState::new(&mut app.world); + let mut mouse_motion = system_state.get_mut(&mut app.world); + + mouse_motion.send(MouseMotion { + delta: Vec2::new(x as f32, y as f32), }); } event::Event::Suspended => { @@ -585,16 +568,12 @@ pub fn winit_runner_with(mut app: App) { winit_state.active = true; } event::Event::MainEventsCleared => { - handle_create_window_events( - &mut app.world, - event_loop, - &mut create_window_event_reader, - ); - let winit_config = app.world.resource::(); + let (winit_config, window_focused_query) = focused_window_state.get(&app.world); + let update = if winit_state.active { - let windows = app.world.resource::(); - let focused = windows.iter().any(|w| w.is_focused()); - match winit_config.update_mode(focused) { + // True if _any_ windows are currently being focused + let app_focused = window_focused_query.iter().any(|window| window.focused); + match winit_config.update_mode(app_focused) { UpdateMode::Continuous | UpdateMode::Reactive { .. } => true, UpdateMode::ReactiveLowPower { .. } => { winit_state.low_power_event @@ -605,6 +584,7 @@ pub fn winit_runner_with(mut app: App) { } else { false }; + if update { winit_state.last_update = Instant::now(); app.update(); @@ -612,12 +592,15 @@ pub fn winit_runner_with(mut app: App) { } Event::RedrawEventsCleared => { { - let winit_config = app.world.resource::(); - let windows = app.world.resource::(); - let focused = windows.iter().any(|w| w.is_focused()); + // Fetch from world + let (winit_config, window_focused_query) = focused_window_state.get(&app.world); + + // True if _any_ windows are currently being focused + let app_focused = window_focused_query.iter().any(|window| window.focused); + let now = Instant::now(); use UpdateMode::*; - *control_flow = match winit_config.update_mode(focused) { + *control_flow = match winit_config.update_mode(app_focused) { Continuous => ControlFlow::Poll, Reactive { max_wait } | ReactiveLowPower { max_wait } => { if let Some(instant) = now.checked_add(*max_wait) { @@ -628,6 +611,7 @@ pub fn winit_runner_with(mut app: App) { } }; } + // This block needs to run after `app.update()` in `MainEventsCleared`. Otherwise, // we won't be able to see redraw requests until the next event, defeating the // purpose of a redraw request! @@ -638,64 +622,18 @@ pub fn winit_runner_with(mut app: App) { redraw = true; } } - if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.iter(app_exit_events).last().is_some() { - *control_flow = ControlFlow::Exit; - } - } + winit_state.redraw_request_sent = redraw; } + _ => (), } }; + // If true, returns control from Winit back to the main Bevy loop if return_from_run { run_return(&mut event_loop, event_handler); } else { run(event_loop, event_handler); } } - -fn handle_create_window_events( - world: &mut World, - event_loop: &EventLoopWindowTarget<()>, - create_window_event_reader: &mut ManualEventReader, -) { - let world = world.cell(); - let mut winit_windows = world.non_send_resource_mut::(); - let mut windows = world.resource_mut::(); - let create_window_events = world.resource::>(); - for create_window_event in create_window_event_reader.iter(&create_window_events) { - let window = winit_windows.create_window( - event_loop, - create_window_event.id, - &create_window_event.descriptor, - ); - // This event is already sent on windows, x11, and xwayland. - // TODO: we aren't yet sure about native wayland, so we might be able to exclude it, - // but sending a duplicate event isn't problematic, as windows already does this. - #[cfg(not(any(target_os = "windows", target_feature = "x11")))] - world.send_event(WindowResized { - id: create_window_event.id, - width: window.width(), - height: window.height(), - }); - windows.add(window); - world.send_event(WindowCreated { - id: create_window_event.id, - }); - - #[cfg(target_arch = "wasm32")] - { - let channel = world.resource_mut::(); - if create_window_event.descriptor.fit_canvas_to_parent { - let selector = if let Some(selector) = &create_window_event.descriptor.canvas { - selector - } else { - web_resize::WINIT_CANVAS_SELECTOR - }; - channel.listen_to_selector(create_window_event.id, selector); - } - } - } -} diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs new file mode 100644 index 0000000000000..74c5b5d7bbc27 --- /dev/null +++ b/crates/bevy_winit/src/system.rs @@ -0,0 +1,277 @@ +use bevy_ecs::{ + entity::Entity, + event::EventWriter, + prelude::{Changed, Component, Resource}, + system::{Commands, NonSendMut, Query, RemovedComponents}, +}; +use bevy_utils::{ + tracing::{error, info, warn}, + HashMap, +}; +use bevy_window::{RawHandleWrapper, Window, WindowClosed, WindowCreated}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + +use winit::{ + dpi::{LogicalSize, PhysicalPosition, PhysicalSize}, + event_loop::EventLoopWindowTarget, +}; + +#[cfg(target_arch = "wasm32")] +use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR}; +use crate::{converters, get_best_videomode, get_fitting_videomode, WinitWindows}; +#[cfg(target_arch = "wasm32")] +use bevy_ecs::system::ResMut; + +/// System responsible for creating new windows whenever a `Window` component is added +/// to an entity. +/// +/// This will default any necessary components if they are not already added. +pub(crate) fn create_window<'a>( + mut commands: Commands, + event_loop: &EventLoopWindowTarget<()>, + created_windows: impl Iterator, + mut event_writer: EventWriter, + mut winit_windows: NonSendMut, + #[cfg(target_arch = "wasm32")] event_channel: ResMut, +) { + for (entity, component) in created_windows { + if winit_windows.get_window(entity).is_some() { + continue; + } + + info!( + "Creating new window {:?} ({:?})", + component.title.as_str(), + entity + ); + + let winit_window = winit_windows.create_window(event_loop, entity, component); + + commands + .entity(entity) + .insert(RawHandleWrapper { + window_handle: winit_window.raw_window_handle(), + display_handle: winit_window.raw_display_handle(), + }) + .insert(PreviousWinitWindow(component.clone())); + + #[cfg(target_arch = "wasm32")] + { + if component.canvas.fit_canvas_to_parent() { + let selector = if let Some(selector) = &component.canvas.canvas() { + selector + } else { + WINIT_CANVAS_SELECTOR + }; + event_channel.listen_to_selector(entity, selector); + } + } + + event_writer.send(WindowCreated { window: entity }); + } +} + +/// Cache for closing windows so we can get better debug information. +#[derive(Debug, Clone, Resource)] +pub struct WindowTitleCache(HashMap); + +pub(crate) fn despawn_window( + closed: RemovedComponents, + mut close_events: EventWriter, + mut winit_windows: NonSendMut, +) { + for window in closed.iter() { + info!("Closing window {:?}", window); + + winit_windows.remove_window(window); + close_events.send(WindowClosed { window }); + } +} + +/// Previous state of the window so we can check sub-portions of what actually was changed. +#[derive(Debug, Clone, Component)] +pub struct PreviousWinitWindow(Window); + +// Detect changes to the window and update the winit window accordingly. +// +// Notes: +// - [`Window::present_mode`] and [`Window::composite_alpha_mode`] updating should be handled in the bevy render crate. +// - [`Window::transparent`] currently cannot be updated after startup for winit. +// - [`Window::canvas`] currently cannot be updated after startup, not entirely sure if it would work well with the +// event channel stuff. +pub(crate) fn changed_window( + mut changed_windows: Query<(Entity, &mut Window, &mut PreviousWinitWindow), Changed>, + winit_windows: NonSendMut, +) { + for (entity, mut window, mut previous_window) in &mut changed_windows { + let previous = &previous_window.0; + + if let Some(winit_window) = winit_windows.get_window(entity) { + if window.title != previous.title { + winit_window.set_title(window.title.as_str()); + } + + if window.mode != previous.mode { + match window.mode { + bevy_window::WindowMode::BorderlessFullscreen => { + winit_window + .set_fullscreen(Some(winit::window::Fullscreen::Borderless(None))); + } + bevy_window::WindowMode::Fullscreen => { + winit_window.set_fullscreen(Some(winit::window::Fullscreen::Exclusive( + get_best_videomode(&winit_window.current_monitor().unwrap()), + ))); + } + bevy_window::WindowMode::SizedFullscreen => winit_window.set_fullscreen(Some( + winit::window::Fullscreen::Exclusive(get_fitting_videomode( + &winit_window.current_monitor().unwrap(), + window.resolution.width() as u32, + window.resolution.height() as u32, + )), + )), + bevy_window::WindowMode::Windowed => winit_window.set_fullscreen(None), + } + } + + if window.resolution != previous.resolution { + let physical_size = PhysicalSize::new( + window.resolution.physical_width() as f64, + window.resolution.physical_height() as f64, + ); + winit_window.set_inner_size(physical_size); + } + + if window.cursor_position != previous.cursor_position { + if let Some(physical_position) = window.cursor_position { + let inner_size = winit_window.inner_size(); + + let position = PhysicalPosition::new( + physical_position.x, + // Flip the coordinate space back to winit's context. + inner_size.height as f64 - physical_position.y, + ); + + if let Err(err) = winit_window.set_cursor_position(position) { + error!("could not set cursor position: {:?}", err); + } + } + } + + if window.cursor != previous.cursor { + winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon)); + + let grab_result = match window.cursor.grab_mode { + bevy_window::CursorGrabMode::None => { + winit_window.set_cursor_grab(winit::window::CursorGrabMode::None) + } + bevy_window::CursorGrabMode::Confined => winit_window + .set_cursor_grab(winit::window::CursorGrabMode::Confined) + .or_else(|_e| { + winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked) + }), + bevy_window::CursorGrabMode::Locked => winit_window + .set_cursor_grab(winit::window::CursorGrabMode::Locked) + .or_else(|_e| { + winit_window.set_cursor_grab(winit::window::CursorGrabMode::Confined) + }), + }; + + if let Err(err) = grab_result { + let err_desc = match window.cursor.grab_mode { + bevy_window::CursorGrabMode::Confined + | bevy_window::CursorGrabMode::Locked => "grab", + bevy_window::CursorGrabMode::None => "ungrab", + }; + + error!("Unable to {} cursor: {}", err_desc, err); + } + + winit_window.set_cursor_visible(window.cursor.visible); + } + + if window.decorations != previous.decorations { + winit_window.set_decorations(window.decorations); + } + + if window.resizable != previous.resizable { + winit_window.set_resizable(window.resizable); + } + + if window.resize_constraints != previous.resize_constraints { + let constraints = window.resize_constraints.check_constraints(); + let min_inner_size = LogicalSize { + width: constraints.min_width, + height: constraints.min_height, + }; + let max_inner_size = LogicalSize { + width: constraints.max_width, + height: constraints.max_height, + }; + + winit_window.set_min_inner_size(Some(min_inner_size)); + if constraints.max_width.is_finite() && constraints.max_height.is_finite() { + winit_window.set_max_inner_size(Some(max_inner_size)); + } + } + + if window.resolution != previous.resolution || window.position != previous.position { + if let Some(position) = crate::winit_window_position( + &window.position, + &window.resolution, + winit_window.available_monitors(), + winit_window.primary_monitor(), + winit_window.current_monitor(), + ) { + winit_window.set_outer_position(position); + } + } + + if window.state.requesting_maximize() { + winit_window.set_maximized(window.state.is_maximized()); + } else if window.state.requesting_minimize() { + winit_window.set_minimized(true); + } + + window + .state + .set_maximize_by_backend(winit_window.is_maximized()); + window.state.clear_requests(); + + if window.focused != previous.focused && window.focused { + winit_window.focus_window(); + } + + if window.always_on_top != previous.always_on_top { + winit_window.set_always_on_top(window.always_on_top); + } + + if window.hit_test != previous.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.hit_test) { + window.hit_test = previous.hit_test; + warn!( + "Could not set cursor hit test for window {:?}: {:?}", + window.title, err + ); + } + } + + // Currently unsupported changes + if window.transparent != previous.transparent { + window.transparent = previous.transparent; + warn!( + "Winit does not currently support updating transparency after window creation." + ); + } + + #[cfg(target_arch = "wasm32")] + if window.canvas != previous.canvas { + window.canvas = previous.canvas.clone(); + warn!( + "Bevy currently doesn't support modifying the window canvas after initialization." + ); + } + + *previous_window = PreviousWinitWindow(window.clone()); + } + } +} diff --git a/crates/bevy_winit/src/web_resize.rs b/crates/bevy_winit/src/web_resize.rs index 7e289afdfc675..52b55a83d6d26 100644 --- a/crates/bevy_winit/src/web_resize.rs +++ b/crates/bevy_winit/src/web_resize.rs @@ -1,7 +1,6 @@ use crate::WinitWindows; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; -use bevy_window::WindowId; use crossbeam_channel::{Receiver, Sender}; use wasm_bindgen::JsCast; use winit::dpi::LogicalSize; @@ -17,7 +16,7 @@ impl Plugin for CanvasParentResizePlugin { struct ResizeEvent { size: LogicalSize, - window_id: WindowId, + window: Entity, } #[derive(Resource)] @@ -31,7 +30,7 @@ fn canvas_parent_resize_event_handler( resize_events: Res, ) { for event in resize_events.receiver.try_iter() { - if let Some(window) = winit_windows.get_window(event.window_id) { + if let Some(window) = winit_windows.get_window(event.window) { window.set_inner_size(event.size); } } @@ -59,12 +58,12 @@ impl Default for CanvasParentResizeEventChannel { } impl CanvasParentResizeEventChannel { - pub(crate) fn listen_to_selector(&self, window_id: WindowId, selector: &str) { + pub(crate) fn listen_to_selector(&self, window: Entity, selector: &str) { let sender = self.sender.clone(); let owned_selector = selector.to_string(); let resize = move || { if let Some(size) = get_size(&owned_selector) { - sender.send(ResizeEvent { size, window_id }).unwrap(); + sender.send(ResizeEvent { size, window }).unwrap(); } }; diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 38ff59d348daa..3c1ed79784604 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,20 +1,19 @@ -use bevy_math::{DVec2, IVec2}; -use bevy_utils::HashMap; -use bevy_window::{ - CursorGrabMode, MonitorSelection, RawHandleWrapper, Window, WindowDescriptor, WindowId, - WindowMode, -}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use crate::converters::convert_cursor_grab_mode; +use bevy_ecs::entity::Entity; + +use bevy_utils::{tracing::warn, HashMap}; +use bevy_window::{CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution}; + use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - window::Fullscreen, + dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, + monitor::MonitorHandle, }; #[derive(Debug, Default)] pub struct WinitWindows { pub windows: HashMap, - pub window_id_to_winit: HashMap, - pub winit_to_window_id: HashMap, + pub entity_to_winit: HashMap, + pub winit_to_entity: HashMap, // Some winit functions, such as `set_window_icon` can only be used from the main thread. If // they are used in another thread, the app will hang. This marker ensures `WinitWindows` is // only ever accessed with bevy's non-send functions and in NonSend systems. @@ -25,59 +24,46 @@ impl WinitWindows { pub fn create_window( &mut self, event_loop: &winit::event_loop::EventLoopWindowTarget<()>, - window_id: WindowId, - window_descriptor: &WindowDescriptor, - ) -> Window { + entity: Entity, + component: &Window, + ) -> &winit::window::Window { let mut winit_window_builder = winit::window::WindowBuilder::new(); - let &WindowDescriptor { - width, - height, - position, - monitor, - scale_factor_override, - .. - } = window_descriptor; - - let logical_size = LogicalSize::new(width, height); - - let monitor = match monitor { - MonitorSelection::Current => None, - MonitorSelection::Primary => event_loop.primary_monitor(), - MonitorSelection::Index(i) => event_loop.available_monitors().nth(i), - }; - - let selected_or_primary_monitor = monitor.clone().or_else(|| event_loop.primary_monitor()); - - winit_window_builder = match window_descriptor.mode { - WindowMode::BorderlessFullscreen => winit_window_builder - .with_fullscreen(Some(Fullscreen::Borderless(selected_or_primary_monitor))), - WindowMode::Fullscreen => winit_window_builder.with_fullscreen(Some( - Fullscreen::Exclusive(get_best_videomode(&selected_or_primary_monitor.unwrap())), + winit_window_builder = match component.mode { + WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some( + winit::window::Fullscreen::Borderless(event_loop.primary_monitor()), )), WindowMode::SizedFullscreen => winit_window_builder.with_fullscreen(Some( - Fullscreen::Exclusive(get_fitting_videomode( - &selected_or_primary_monitor.unwrap(), - window_descriptor.width as u32, - window_descriptor.height as u32, + winit::window::Fullscreen::Exclusive(get_fitting_videomode( + &event_loop.primary_monitor().unwrap(), + component.resolution.width() as u32, + component.resolution.height() as u32, )), )), - WindowMode::Windowed => { - if let Some(sf) = scale_factor_override { - winit_window_builder.with_inner_size(logical_size.to_physical::(sf)) - } else { - winit_window_builder.with_inner_size(logical_size) + _ => { + if let Some(position) = winit_window_position( + &component.position, + &component.resolution, + event_loop.available_monitors(), + event_loop.primary_monitor(), + None, + ) { + winit_window_builder = winit_window_builder.with_position(position); } + + winit_window_builder.with_inner_size( + LogicalSize::new(component.resolution.width(), component.resolution.height()) + .to_physical::(component.resolution.scale_factor()), + ) } }; winit_window_builder = winit_window_builder - .with_resizable(window_descriptor.resizable) - .with_decorations(window_descriptor.decorations) - .with_transparent(window_descriptor.transparent) - .with_always_on_top(window_descriptor.always_on_top); + .with_resizable(component.resizable) + .with_decorations(component.decorations) + .with_transparent(component.transparent); - let constraints = window_descriptor.resize_constraints.check_constraints(); + let constraints = component.resize_constraints.check_constraints(); let min_inner_size = LogicalSize { width: constraints.min_width, height: constraints.min_height, @@ -97,14 +83,14 @@ impl WinitWindows { }; #[allow(unused_mut)] - let mut winit_window_builder = winit_window_builder.with_title(&window_descriptor.title); + let mut winit_window_builder = winit_window_builder.with_title(component.title.as_str()); #[cfg(target_arch = "wasm32")] { use wasm_bindgen::JsCast; use winit::platform::web::WindowBuilderExtWebSys; - if let Some(selector) = &window_descriptor.canvas { + if let Some(selector) = &component.canvas.canvas() { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let canvas = document @@ -121,59 +107,33 @@ impl WinitWindows { let winit_window = winit_window_builder.build(event_loop).unwrap(); - if window_descriptor.mode == WindowMode::Windowed { - use bevy_window::WindowPosition::*; - match position { - Automatic => { - if let Some(monitor) = monitor { - winit_window.set_outer_position(monitor.position()); - } - } - Centered => { - if let Some(monitor) = monitor.or_else(|| winit_window.current_monitor()) { - let monitor_position = monitor.position().cast::(); - let size = monitor.size(); - - // Logical to physical window size - let PhysicalSize:: { width, height } = - logical_size.to_physical(monitor.scale_factor()); - - let position = PhysicalPosition { - x: size.width.saturating_sub(width) as f64 / 2. + monitor_position.x, - y: size.height.saturating_sub(height) as f64 / 2. + monitor_position.y, - }; - - winit_window.set_outer_position(position); - } - } - At(position) => { - if let Some(monitor) = monitor.or_else(|| winit_window.current_monitor()) { - let monitor_position = DVec2::from(<(_, _)>::from(monitor.position())); - let position = monitor_position + position.as_dvec2(); - - if let Some(sf) = scale_factor_override { - winit_window.set_outer_position( - LogicalPosition::new(position.x, position.y).to_physical::(sf), - ); - } else { - winit_window - .set_outer_position(LogicalPosition::new(position.x, position.y)); - } - } + match component.cursor.grab_mode { + CursorGrabMode::Confined | CursorGrabMode::Locked => { + match winit_window + .set_cursor_grab(convert_cursor_grab_mode(component.cursor.grab_mode)) + { + Ok(_) | Err(winit::error::ExternalError::NotSupported(_)) => {} + Err(err) => Err(err).unwrap(), } } + _ => {} + } + + // Do not set the grab mode on window creation if it's none, this can fail on mobile + if window_descriptor.cursor_grab_mode != CursorGrabMode::None { + window.set_cursor_grab_mode(window_descriptor.cursor_grab_mode); } - winit_window.set_cursor_visible(window_descriptor.cursor_visible); + winit_window.set_cursor_visible(component.cursor.visible); - self.window_id_to_winit.insert(window_id, winit_window.id()); - self.winit_to_window_id.insert(winit_window.id(), window_id); + self.entity_to_winit.insert(entity, winit_window.id()); + self.winit_to_entity.insert(winit_window.id(), entity); #[cfg(target_arch = "wasm32")] { use winit::platform::web::WindowExtWebSys; - if window_descriptor.canvas.is_none() { + if component.canvas.canvas().is_none() { let canvas = winit_window.canvas(); let window = web_sys::window().unwrap(); @@ -185,45 +145,31 @@ impl WinitWindows { } } - let position = winit_window - .outer_position() - .ok() - .map(|position| IVec2::new(position.x, position.y)); - let inner_size = winit_window.inner_size(); - let scale_factor = winit_window.scale_factor(); - let raw_handle = RawHandleWrapper { - window_handle: winit_window.raw_window_handle(), - display_handle: winit_window.raw_display_handle(), - }; - self.windows.insert(winit_window.id(), winit_window); - let mut window = Window::new( - window_id, - window_descriptor, - inner_size.width, - inner_size.height, - scale_factor, - position, - Some(raw_handle), - ); - // Do not set the grab mode on window creation if it's none, this can fail on mobile - if window_descriptor.cursor_grab_mode != CursorGrabMode::None { - window.set_cursor_grab_mode(window_descriptor.cursor_grab_mode); - } - window + self.windows + .entry(winit_window.id()) + .insert(winit_window) + .into_mut() } - pub fn get_window(&self, id: WindowId) -> Option<&winit::window::Window> { - self.window_id_to_winit - .get(&id) - .and_then(|id| self.windows.get(id)) + /// Get the winit window that is associated with our entity. + pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> { + self.entity_to_winit + .get(&entity) + .and_then(|winit_id| self.windows.get(winit_id)) } - pub fn get_window_id(&self, id: winit::window::WindowId) -> Option { - self.winit_to_window_id.get(&id).cloned() + /// Get the entity associated with the winit window id. + /// + /// This is mostly just an intermediary step between us and winit. + pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option { + self.winit_to_entity.get(&winit_id).cloned() } - pub fn remove_window(&mut self, id: WindowId) -> Option { - let winit_id = self.window_id_to_winit.remove(&id)?; + /// Remove a window from winit. + /// + /// This should mostly just be called when the window is closing. + pub fn remove_window(&mut self, entity: Entity) -> Option { + let winit_id = self.entity_to_winit.remove(&entity)?; // Don't remove from winit_to_window_id, to track that we used to know about this winit window self.windows.remove(&winit_id) } @@ -278,3 +224,71 @@ pub fn get_best_videomode(monitor: &winit::monitor::MonitorHandle) -> winit::mon modes.first().unwrap().clone() } + +// Ideally we could generify this across window backends, but we only really have winit atm +// so whatever. +// +// Also ends up bringing to question the standardization of physical/logical position/sizes +// into specific types so we don't run into weird cross bugs where we use a logical position +// in a physical context. +pub fn winit_window_position( + position: &WindowPosition, + resolution: &WindowResolution, + mut available_monitors: impl Iterator, + primary_monitor: Option, + current_monitor: Option, +) -> Option> { + match position { + WindowPosition::Automatic => { + /* Window manager will handle position */ + None + } + WindowPosition::Centered(monitor_selection) => { + use bevy_window::MonitorSelection::*; + let maybe_monitor = match monitor_selection { + Current => { + if current_monitor.is_none() { + warn!("Can't select current monitor on window creation or cannot find current monitor!"); + } + current_monitor + } + Primary => primary_monitor, + Index(n) => available_monitors.nth(*n), + }; + + if let Some(monitor) = maybe_monitor { + let screen_size = monitor.size(); + + let scale_factor = resolution.base_scale_factor(); + + // Logical to physical window size + let (width, height): (u32, u32) = + LogicalSize::new(resolution.width(), resolution.height()) + .to_physical::(scale_factor) + .into(); + + let position = PhysicalPosition { + x: screen_size.width.saturating_sub(width) as f64 / 2. + + monitor.position().x as f64, + y: screen_size.height.saturating_sub(height) as f64 / 2. + + monitor.position().y as f64, + }; + + Some(position) + } else { + warn!("Couldn't get monitor selected with: {monitor_selection:?}"); + None + } + } + WindowPosition::At(position) => Some( + LogicalPosition::new(position[0] as f64, position[1] as f64) + .to_physical::(resolution.base_scale_factor()), + ), + } +} + +// WARNING: this only works under the assumption that wasm runtime is single threaded +#[cfg(target_arch = "wasm32")] +unsafe impl Send for WinitWindows {} +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for WinitWindows {} diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index d2741efd65784..2ff5b299e7cc7 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -3,10 +3,8 @@ use std::f32::consts::PI; use bevy::{ - core_pipeline::clear_color::ClearColorConfig, - prelude::*, - render::camera::Viewport, - window::{WindowId, WindowResized}, + core_pipeline::clear_color::ClearColorConfig, prelude::*, render::camera::Viewport, + window::WindowResized, }; fn main() { @@ -82,7 +80,7 @@ struct LeftCamera; struct RightCamera; fn set_camera_viewports( - windows: Res, + windows: Query<&Window>, mut resize_events: EventReader, mut left_camera: Query<&mut Camera, (With, Without)>, mut right_camera: Query<&mut Camera, With>, @@ -91,21 +89,25 @@ fn set_camera_viewports( // so then each camera always takes up half the screen. // A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup. for resize_event in resize_events.iter() { - if resize_event.id == WindowId::primary() { - let window = windows.primary(); - let mut left_camera = left_camera.single_mut(); - left_camera.viewport = Some(Viewport { - physical_position: UVec2::new(0, 0), - physical_size: UVec2::new(window.physical_width() / 2, window.physical_height()), - ..default() - }); + let window = windows.get(resize_event.window).unwrap(); + let mut left_camera = left_camera.single_mut(); + left_camera.viewport = Some(Viewport { + physical_position: UVec2::new(0, 0), + physical_size: UVec2::new( + window.resolution.physical_width() / 2, + window.resolution.physical_height(), + ), + ..default() + }); - let mut right_camera = right_camera.single_mut(); - right_camera.viewport = Some(Viewport { - physical_position: UVec2::new(window.physical_width() / 2, 0), - physical_size: UVec2::new(window.physical_width() / 2, window.physical_height()), - ..default() - }); - } + let mut right_camera = right_camera.single_mut(); + right_camera.viewport = Some(Viewport { + physical_position: UVec2::new(window.resolution.physical_width() / 2, 0), + physical_size: UVec2::new( + window.resolution.physical_width() / 2, + window.resolution.physical_height(), + ), + ..default() + }); } } diff --git a/examples/app/return_after_run.rs b/examples/app/return_after_run.rs index 966659688655e..d7ff3ee622ad9 100644 --- a/examples/app/return_after_run.rs +++ b/examples/app/return_after_run.rs @@ -1,6 +1,6 @@ //! Shows how to return to the calling function after a windowed Bevy app has exited. -use bevy::{prelude::*, winit::WinitSettings}; +use bevy::{prelude::*, window::WindowPlugin, winit::WinitSettings}; fn main() { println!("Running Bevy App"); @@ -10,10 +10,10 @@ fn main() { ..default() }) .add_plugins(DefaultPlugins.set(WindowPlugin { - window: WindowDescriptor { - title: "Close the window to return to the main function".to_owned(), + primary_window: Some(Window { + title: "Close the window to return to the main function".into(), ..default() - }, + }), ..default() })) .add_system(system) diff --git a/examples/ecs/parallel_query.rs b/examples/ecs/parallel_query.rs index 992e0bcc894e9..afc5784099217 100644 --- a/examples/ecs/parallel_query.rs +++ b/examples/ecs/parallel_query.rs @@ -1,6 +1,9 @@ //! Illustrates parallel queries with `ParallelIterator`. -use bevy::prelude::*; +use bevy::{ + prelude::*, + window::{PrimaryWindow, Window}, +}; use rand::random; #[derive(Component, Deref)] @@ -37,10 +40,13 @@ fn move_system(mut sprites: Query<(&mut Transform, &Velocity)>) { } // Bounce sprites outside the window -fn bounce_system(windows: Res, mut sprites: Query<(&Transform, &mut Velocity)>) { - let window = windows.primary(); - let width = window.width(); - let height = window.height(); +fn bounce_system( + primary_window: Query<&Window, With>, + mut sprites: Query<(&Transform, &mut Velocity)>, +) { + let window = primary_window.single(); + let width = window.resolution.width(); + let height = window.resolution.height(); let left = width / -2.0; let right = width / 2.0; let bottom = height / -2.0; diff --git a/examples/games/contributors.rs b/examples/games/contributors.rs index 8f452112ac667..9e741e3bf505d 100644 --- a/examples/games/contributors.rs +++ b/examples/games/contributors.rs @@ -1,6 +1,10 @@ //! This example displays each contributor to the bevy source code as a bouncing bevy-ball. -use bevy::{prelude::*, utils::HashSet}; +use bevy::{ + prelude::*, + utils::HashSet, + window::{PrimaryWindow, Window}, +}; use rand::{prelude::SliceRandom, Rng}; use std::{ env::VarError, @@ -245,21 +249,19 @@ fn velocity_system(time: Res