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 b2e80a42bddcb..671369d14ff6f 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -38,6 +38,7 @@ pub mod prelude { }; } +use bevy_window::{PrimaryWindow, RawHandleWrapper}; use globals::GlobalsPlugin; pub use once_cell; @@ -52,7 +53,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, @@ -140,17 +141,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 ffb6c1761b082..1caac7c5b26db 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -284,10 +284,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..6a5ed74089486 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -5,6 +5,7 @@ use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, + prelude::With, reflect::ReflectComponent, system::{Commands, Local, Query, Res, ResMut}, }; @@ -19,7 +20,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 +70,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 +82,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 +151,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 +167,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..c2fce0c2996cd 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.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..9ca3aed824369 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; @@ -297,7 +297,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 +310,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..6993f30b4f340 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -4,34 +4,33 @@ 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, WindowPlugin, 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 +38,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 +69,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 +83,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 +122,41 @@ 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 `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..ddd2e726409b7 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) { - if windows.iter().count() == 0 { +pub fn exit_on_all_closed(mut app_exit_events: EventWriter, windows: Query<&Window>) { + if windows.is_empty() { + 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..b68a00e10c54b 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,137 +1,233 @@ -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_value(Serialize, Deserialize) + reflect(Serialize, Deserialize) )] -pub struct WindowId(Uuid); +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), +} -/// Presentation mode for a window. -/// -/// The presentation mode specifies when a frame is presented to the window. The `Fifo` -/// option corresponds to a traditional `VSync`, where the framerate is capped by the -/// display refresh rate. Both `Immediate` and `Mailbox` are low-latency and are not -/// capped by the refresh rate, but may not be available on all platforms. Tearing -/// may be observed with `Immediate` mode, but will not be observed with `Mailbox` or -/// `Fifo`. -/// -/// `AutoVsync` or `AutoNoVsync` will gracefully fallback to `Fifo` when unavailable. -/// -/// `Immediate` or `Mailbox` will panic if not supported by the platform. +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. /// -/// 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). +/// For most purposes you probably want to use the unnormalized version [`WindowRef`]. #[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect, FromReflect)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Hash)] -#[doc(alias = "vsync")] -pub enum PresentMode { - /// Chooses FifoRelaxed -> Fifo based on availability. - /// - /// Because of the fallback behavior, it is supported everywhere. - AutoVsync = 0, - /// Chooses Immediate -> Mailbox -> Fifo (on web) based on availability. - /// - /// Because of the fallback behavior, it is supported everywhere. - AutoNoVsync = 1, - /// The presentation engine does **not** wait for a vertical blanking period and - /// the request is presented immediately. This is a low-latency presentation mode, - /// but visible tearing may be observed. Not optimal for mobile. - /// - /// Selecting this variant will panic if not supported, it is preferred to use - /// [`PresentMode::AutoNoVsync`]. - Immediate = 2, - /// The presentation engine waits for the next vertical blanking period to update - /// the current image, but frames may be submitted without delay. This is a low-latency - /// presentation mode and visible tearing will **not** be observed. Not optimal for mobile. - /// - /// Selecting this variant will panic if not supported, it is preferred to use - /// [`PresentMode::AutoNoVsync`]. - Mailbox = 3, - /// 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. - Fifo = 4, // NOTE: The explicit ordinal values mirror wgpu. +pub struct NormalizedWindowRef(Entity); + +impl NormalizedWindowRef { + /// Fetch the entity of this window reference + pub fn entity(&self) -> Entity { + self.0 + } } -/// Specifies how the alpha channel of the textures should be handled during compositing. -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, FromReflect)] +/// 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(Debug, PartialEq, Hash)] -pub enum CompositeAlphaMode { - /// Chooses either `Opaque` or `Inherit` automatically, depending on the - /// `alpha_mode` that the current surface can support. - 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 - /// constant alpha of 1.0. - Opaque = 1, - /// The alpha channel, if it exists, of the textures is respected in the - /// compositing process. The non-alpha channels of the textures are - /// expected to already be multiplied by the alpha channel by the - /// application. - PreMultiplied = 2, - /// The alpha channel, if it exists, of the textures is respected in the - /// compositing process. The non-alpha channels of the textures are not - /// expected to already be multiplied by the alpha channel by the - /// application; instead, the compositor will multiply the non-alpha - /// channels of the texture by the alpha channel during compositing. - PostMultiplied = 3, - /// The alpha channel, if it exists, of the textures is unknown for processing - /// during compositing. Instead, the application is responsible for setting - /// the composite alpha blending mode using native WSI command. If not set, - /// then a platform-specific default will be used. - Inherit = 4, +#[reflect(Component, Default)] +pub struct Window { + /// The cursor of this window. + pub cursor: Cursor, + /// 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, + /// 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, + /// Should the window always be on top of other windows? + /// + /// ## Platform-specific + /// + /// - iOS / Android / Web / Wayland: Unsupported. + pub always_on_top: 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, + /// Stores internal state that isn't directly accessible. + pub internal: InternalWindowState, +} + +impl Default for Window { + fn default() -> Self { + Self { + title: "Bevy App".to_owned(), + cursor: Default::default(), + present_mode: Default::default(), + mode: Default::default(), + position: Default::default(), + resolution: Default::default(), + internal: Default::default(), + composite_alpha_mode: Default::default(), + resize_constraints: Default::default(), + resizable: true, + decorations: true, + transparent: false, + focused: true, + always_on_top: false, + fit_canvas_to_parent: false, + canvas: None, + } + } } -impl WindowId { - /// Creates a new [`WindowId`]. - pub fn new() -> Self { - WindowId(Uuid::new_v4()) +impl Window { + /// Setting this to true will attempt to maximize the window. + /// + /// Setting it to false will attempt to un-maximize the window. + pub fn set_maximized(&mut self, maximized: bool) { + self.internal.maximize_request = Some(maximized); } - /// The [`WindowId`] for the primary window. - pub const fn primary() -> Self { - WindowId(Uuid::from_u128(0)) + + /// Setting this to true will attempt to maximize the window. + /// + /// Setting it to false will attempt to un-maximize the window. + pub fn set_minimized(&mut self, minimized: bool) { + self.internal.minimize_request = Some(minimized); } - /// Get whether or not this [`WindowId`] is for the primary window. - pub fn is_primary(&self) -> bool { - *self == WindowId::primary() + + /// The window's client area width in logical pixels. + #[inline] + pub fn width(&self) -> f32 { + self.resolution.width() } -} -use crate::CursorIcon; -use std::fmt; + /// The window's client area height in logical pixels. + #[inline] + pub fn height(&self) -> f32 { + self.resolution.height() + } -use crate::raw_handle::RawHandleWrapper; + /// The window's client area width in physical pixels. + #[inline] + pub fn physical_width(&self) -> u32 { + self.resolution.physical_width() + } -impl fmt::Display for WindowId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.as_simple().fmt(f) + /// The window's client area height in physical pixels. + #[inline] + pub fn physical_height(&self) -> u32 { + self.resolution.physical_height() } -} -impl Default for WindowId { - fn default() -> Self { - WindowId::primary() + /// The window's scale factor. + #[inline] + pub fn scale_factor(&self) -> f64 { + self.resolution.scale_factor() } } @@ -150,9 +246,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 +268,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,454 +304,177 @@ impl WindowResizeConstraints { } } -/// An operating system window that can present content and receive user input. -/// -/// To create a window, use a [`EventWriter`](`crate::CreateWindow`). -/// -/// ## Window Sizes -/// -/// There are three sizes associated with a window. The physical size which is -/// the height and width in physical pixels on the monitor. The logical size -/// which is the physical size scaled by an operating system provided factor to -/// account for monitors with differing pixel densities or user preference. And -/// the requested size, measured in logical pixels, which is the value submitted -/// to the API when creating the window, or requesting that it be resized. -/// -/// The actual size, in logical pixels, of the window may not match the -/// 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, - 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, -} -/// 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, -} - -/// 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)] +/// Stores data about the window's cursor. +#[derive(Debug, Copy, Clone, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq)] -pub enum CursorGrabMode { - /// The cursor can freely leave the window. - None, - /// The cursor is confined to the window area. - Confined, - /// The cursor is locked inside the window area to a certain position. - Locked, +#[reflect(Debug, 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, + + /// 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, + + /// The position of this window's cursor. + pub position: Option, } -/// Defines the way a window is displayed. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] +impl Default for Cursor { + fn default() -> Self { + Cursor { + icon: CursorIcon::Default, + visible: true, + grab_mode: CursorGrabMode::None, + hit_test: true, + position: None, + } + } +} + +/// 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 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. +pub enum WindowPosition { + /// Position will be set by the window manager + #[default] + Automatic, + /// Window will be centered on the selected monitor /// - /// 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, + /// 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 physical pixels) + /// + /// (0,0) represents top-left corner of screen space. + At(IVec2), } -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, - } - } - /// Get the window's [`WindowId`]. - #[inline] - pub fn id(&self) -> WindowId { - self.id - } - - /// The current logical width of the window's client area. - #[inline] - pub fn width(&self) -> f32 { - (self.physical_width as f64 / self.scale_factor()) as f32 +impl WindowPosition { + /// Creates a new [`WindowPosition`] at a position. + pub fn new(position: IVec2) -> Self { + Self::At(position) } - /// The current logical height of the window's client area. - #[inline] - pub fn height(&self) -> f32 { - (self.physical_height as f64 / self.scale_factor()) as f32 + /// Set the position to a specific point. + pub fn set(&mut self, position: IVec2) { + *self = WindowPosition::At(position); } - /// 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 + /// Set the window to a specific monitor. + pub fn center(&mut self, monitor: MonitorSelection) { + *self = WindowPosition::Centered(monitor); } +} - /// 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 - } - - /// The window's client area width in physical pixels. - #[inline] - pub fn physical_width(&self) -> u32 { - self.physical_width - } - - /// The window's client area height in physical pixels. - #[inline] - pub fn physical_height(&self) -> u32 { - self.physical_height - } - - /// The window's client resize constraint in logical pixels. - #[inline] - pub fn resize_constraints(&self) -> WindowResizeConstraints { - self.resize_constraints - } - - /// The window's client position in physical pixels. - #[inline] - pub fn position(&self) -> Option { - self.position - } - /// Set whether or not the window is maximized. - #[inline] - pub fn set_maximized(&mut self, maximized: bool) { - self.command_queue - .push(WindowCommand::SetMaximized { maximized }); - } +/// ## Window Sizes +/// +/// There are three sizes associated with a window. The physical size which is +/// the height and width in physical pixels on the monitor. The logical size +/// which is the physical size scaled by an operating system provided factor to +/// account for monitors with differing pixel densities or user preference. And +/// the requested size, measured in logical pixels, which is the value submitted +/// to the API when creating the window, or requesting that it be resized. +/// +/// The actual size, in logical pixels, of the window may not match the +/// 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. +#[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, + scale_factor_override: Option, + scale_factor: f64, +} - /// Sets the window to minimized or back. - /// - /// # Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - Wayland: Un-minimize is unsupported. - #[inline] - pub fn set_minimized(&mut self, minimized: bool) { - self.command_queue - .push(WindowCommand::SetMinimized { minimized }); +impl Default for WindowResolution { + fn default() -> Self { + WindowResolution { + physical_width: 1280, + physical_height: 720, + scale_factor_override: None, + scale_factor: 1.0, + } } +} - /// 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. - #[inline] - pub fn set_position(&mut self, monitor: MonitorSelection, position: IVec2) { - self.command_queue.push(WindowCommand::SetPosition { - monitor_selection: monitor, - position, - }); +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() + } } - /// Modifies the position of the window to be in the center of the current monitor - /// - /// # Platform-specific - /// - iOS: Can only be called on the main thread. - /// - Web / Android / Wayland: Unsupported. - #[inline] - pub fn center_window(&mut self, monitor_selection: MonitorSelection) { - self.command_queue - .push(WindowCommand::Center(monitor_selection)); + /// 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 } - /// Modifies the minimum and maximum window bounds for resizing in logical pixels. + /// The window's client area width in logical pixels. #[inline] - pub fn set_resize_constraints(&mut self, resize_constraints: WindowResizeConstraints) { - self.command_queue - .push(WindowCommand::SetResizeConstraints { resize_constraints }); - } - - /// 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(), - }); - } - - /// 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(), - }); + pub fn width(&self) -> f32 { + (self.physical_width() as f64 / self.scale_factor()) as f32 } - #[allow(missing_docs)] + /// The window's client area width in logical pixels. #[inline] - pub fn update_scale_factor_from_backend(&mut self, scale_factor: f64) { - self.backend_scale_factor = scale_factor; + pub fn height(&self) -> f32 { + (self.physical_height() as f64 / self.scale_factor()) as f32 } - #[allow(missing_docs)] + /// The window's client area width in physical pixels. #[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; + pub fn physical_width(&self) -> u32 { + self.physical_width } - #[allow(missing_docs)] + /// The window's client area height in physical pixels. #[inline] - pub fn update_actual_position_from_backend(&mut self, position: IVec2) { - self.position = Some(position); + pub fn physical_height(&self) -> u32 { + self.physical_height } /// The ratio of physical pixels to logical pixels @@ -656,300 +482,142 @@ impl Window { /// `physical_pixels = logical_pixels * scale_factor` pub fn scale_factor(&self) -> f64 { self.scale_factor_override - .unwrap_or(self.backend_scale_factor) + .unwrap_or_else(|| self.base_scale_factor()) } /// The window scale factor as reported by the window backend. /// - /// This value is unaffected by [`scale_factor_override`](Window::scale_factor_override). + /// This value is unaffected by [`WindowResolution::scale_factor_override`]. #[inline] - pub fn backend_scale_factor(&self) -> f64 { - self.backend_scale_factor + pub fn base_scale_factor(&self) -> f64 { + self.scale_factor } - /// The scale factor set with [`set_scale_factor_override`](Window::set_scale_factor_override). + + /// The scale factor set with [`WindowResolution::set_scale_factor_override`]. /// /// 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 }); - } - - #[inline] - #[doc(alias = "vsync")] - /// Get the window's [`PresentMode`]. - pub fn present_mode(&self) -> PresentMode { - self.present_mode - } + /// Set the window's logical resolution. #[inline] - /// Get the window's [`CompositeAlphaMode`]. - pub fn alpha_mode(&self) -> CompositeAlphaMode { - self.alpha_mode + 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, + ); } - #[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 + /// Set the window's physical resolution. /// - /// - **`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 + /// This will ignore the scale factor setting, so most of the time you should + /// prefer to use [`WindowResolution::set`]. #[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 }); + pub fn set_physical_resolution(&mut self, width: u32, height: u32) { + self.physical_width = width; + self.physical_height = height; } - /// The current mouse position, in physical pixels. + /// Set the window's scale factor, this may get overriden by the backend. #[inline] - pub fn physical_cursor_position(&self) -> Option { - self.physical_cursor_position + pub fn set_scale_factor(&mut self, scale_factor: f64) { + let (width, height) = (self.width(), self.height()); + self.scale_factor = scale_factor; + self.set(width, height); } - /// 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 }); - } - /// Modifies whether the window catches cursor events. - /// - /// 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. + /// Set the window's scale factor, this will be used over what the backend decides. #[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; + pub fn set_scale_factor_override(&mut self, scale_factor_override: Option) { + let (width, height) = (self.width(), self.height()); + self.scale_factor_override = scale_factor_override; + self.set(width, height); } +} - #[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 +impl From<(I, I)> for WindowResolution +where + I: Into, +{ + fn from((width, height): (I, I)) -> WindowResolution { + WindowResolution::new(width.into(), height.into()) } +} - /// 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 From<[I; 2]> for WindowResolution +where + I: Into, +{ + fn from([width, height]: [I; 2]) -> WindowResolution { + WindowResolution::new(width.into(), height.into()) } +} - /// 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. - #[inline] - pub fn canvas(&self) -> Option<&str> { - self.canvas.as_deref() +impl From for WindowResolution { + fn from(res: bevy_math::Vec2) -> WindowResolution { + WindowResolution::new(res.x, res.y) } +} - /// 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. - #[inline] - pub fn fit_canvas_to_parent(&self) -> bool { - self.fit_canvas_to_parent +impl From for WindowResolution { + fn from(res: bevy_math::DVec2) -> WindowResolution { + WindowResolution::new(res.x as f32, res.y as f32) } } -/// Defines where window should be placed at on creation. -#[derive(Debug, Clone, Copy, PartialEq, Reflect, FromReflect)] +/// Defines if and 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, 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)] -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), +#[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, + /// The cursor is locked inside the window area to a certain position. + Locked, +} + +/// Stores internal state that isn't directly accessible. +#[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 InternalWindowState { + /// If this is true then next frame we will ask to minimize the window. + minimize_request: Option, + /// If this is true then next frame we will ask to maximize/un-maximize the window depending on `maximized`. + maximize_request: Option, +} + +impl InternalWindowState { + /// Consumes the current maximize request, if it exists. This should only be called by window backends. + pub fn take_maximize_request(&mut self) -> Option { + self.maximize_request.take() + } + + /// Consumes the current minimize request, if it exists. This should only be called by window backends. + pub fn take_minimize_request(&mut self) -> Option { + self.minimize_request.take() + } } /// Defines which monitor to use. @@ -971,129 +639,110 @@ pub enum MonitorSelection { Index(usize), } -/// Describes the information needed for creating a window. +/// Presentation mode for 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. +/// The presentation mode specifies when a frame is presented to the window. The `Fifo` +/// option corresponds to a traditional `VSync`, where the framerate is capped by the +/// display refresh rate. Both `Immediate` and `Mailbox` are low-latency and are not +/// capped by the refresh rate, but may not be available on all platforms. Tearing +/// may be observed with `Immediate` mode, but will not be observed with `Mailbox` or +/// `Fifo`. /// -/// See [`examples/window/window_settings.rs`] for usage. +/// `AutoVsync` or `AutoNoVsync` will gracefully fallback to `Fifo` when unavailable. /// -/// [`examples/window/window_settings.rs`]: https://github.com/bevyengine/bevy/blob/latest/examples/window/window_settings.rs -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +/// `Immediate` or `Mailbox` will panic if not supported by the platform. +#[repr(C)] +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, 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). +#[reflect(Debug, PartialEq, Hash)] +#[doc(alias = "vsync")] +pub enum PresentMode { + /// Chooses FifoRelaxed -> Fifo based on availability. /// - /// 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. + /// Because of the fallback behavior, it is supported everywhere. + AutoVsync = 0, + /// Chooses Immediate -> Mailbox -> Fifo (on web) based on availability. /// - /// **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. + /// Because of the fallback behavior, it is supported everywhere. + AutoNoVsync = 1, + /// The presentation engine does **not** wait for a vertical blanking period and + /// the request is presented immediately. This is a low-latency presentation mode, + /// but visible tearing may be observed. Not optimal for mobile. /// - /// 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. + /// Selecting this variant will panic if not supported, it is preferred to use + /// [`PresentMode::AutoNoVsync`]. + Immediate = 2, + /// The presentation engine waits for the next vertical blanking period to update + /// the current image, but frames may be submitted without delay. This is a low-latency + /// presentation mode and visible tearing will **not** be observed. Not optimal for mobile. /// - /// ## Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - Linux (Wayland): Unsupported. - pub always_on_top: bool, + /// Selecting this variant will panic if not supported, it is preferred to use + /// [`PresentMode::AutoNoVsync`]. + Mailbox = 3, + /// 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. } -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, - } - } +/// Specifies how the alpha channel of the textures should be handled during compositing. +#[repr(C)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Hash)] +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 + /// constant alpha of 1.0. + Opaque = 1, + /// The alpha channel, if it exists, of the textures is respected in the + /// compositing process. The non-alpha channels of the textures are + /// expected to already be multiplied by the alpha channel by the + /// application. + PreMultiplied = 2, + /// The alpha channel, if it exists, of the textures is respected in the + /// compositing process. The non-alpha channels of the textures are not + /// expected to already be multiplied by the alpha channel by the + /// application; instead, the compositor will multiply the non-alpha + /// channels of the texture by the alpha channel during compositing. + PostMultiplied = 3, + /// The alpha channel, if it exists, of the textures is unknown for processing + /// during compositing. Instead, the application is responsible for setting + /// the composite alpha blending mode using native WSI command. If not set, + /// then a platform-specific default will be used. + Inherit = 4, +} + +/// 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, } 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..b3bd60f06a43d 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,266 +1,112 @@ 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}, }; +use crate::system::WinitWindowInfo; +#[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); - } -} - -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(); + .add_system_set_to_stage( + CoreStage::PostUpdate, + SystemSet::new() + .label(ModifiesWindows) + .with_system(changed_window) + .with_system(despawn_window), + ); - 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, &mut 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, &mut 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, mut new_windows, event_writer, winit_windows) = + create_window_system_state.get_mut(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let (commands, event_loop, mut 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_mut(), + event_writer, + winit_windows, + #[cfg(target_arch = "wasm32")] + event_channel, + ); } + + create_window_system_state.apply(&mut app.world); } } @@ -307,8 +153,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 +215,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 +232,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, &mut Window), Added>, + EventWriter, + NonSendMut, + )> = SystemState::from_world(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let mut create_window_system_state: SystemState<( + Commands, + Query<(Entity, &mut 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, mut new_windows, created_window_writer, winit_windows) = + create_window_system_state.get_mut(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let ( + commands, + mut 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_mut(), + 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 +321,120 @@ 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, &mut WinitWindowInfo)>, + 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 (mut window, mut info) = + if let Ok((window, info)) = window_query.get_mut(window_entity) { + (window, info) + } else { + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event ); 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); - 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, + window + .resolution + .set_physical_resolution(size.width, size.height); + info.last_winit_size = size; + + window_events.window_resized.send(WindowResized { + window: window_entity, width: window.width(), height: window.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 +442,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.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 +466,84 @@ 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(), - ) - .to_physical::(forced_factor); + *new_inner_size = + winit::dpi::LogicalSize::new(window.width(), window.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.width(), new_logical_width) + || approx::relative_ne!(window.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 +554,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 +569,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 +585,7 @@ pub fn winit_runner_with(mut app: App) { } else { false }; + if update { winit_state.last_update = Instant::now(); app.update(); @@ -612,12 +593,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 +612,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 +623,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..c8bf85967cd14 --- /dev/null +++ b/crates/bevy_winit/src/system.rs @@ -0,0 +1,284 @@ +use bevy_ecs::{ + entity::Entity, + event::EventWriter, + prelude::{Changed, Component, Resource}, + system::{Commands, NonSendMut, Query, RemovedComponents}, + world::Mut, +}; +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, mut 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); + let current_size = winit_window.inner_size(); + component + .resolution + .set_scale_factor(winit_window.scale_factor()); + commands + .entity(entity) + .insert(RawHandleWrapper { + window_handle: winit_window.raw_window_handle(), + display_handle: winit_window.raw_display_handle(), + }) + .insert(WinitWindowInfo { + previous: component.clone(), + last_winit_size: PhysicalSize { + width: current_size.width, + height: current_size.height, + }, + }); + + #[cfg(target_arch = "wasm32")] + { + if component.fit_canvas_to_parent { + let selector = if let Some(selector) = &component.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 WinitWindowInfo { + pub previous: Window, + pub last_winit_size: PhysicalSize, +} + +// 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 WinitWindowInfo), Changed>, + winit_windows: NonSendMut, +) { + for (entity, mut window, mut info) in &mut changed_windows { + let previous = &info.previous; + + 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 { + let new_mode = match window.mode { + bevy_window::WindowMode::BorderlessFullscreen => { + Some(winit::window::Fullscreen::Borderless(None)) + } + bevy_window::WindowMode::Fullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_best_videomode( + &winit_window.current_monitor().unwrap(), + ))) + } + bevy_window::WindowMode::SizedFullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_fitting_videomode( + &winit_window.current_monitor().unwrap(), + window.width() as u32, + window.height() as u32, + ))) + } + bevy_window::WindowMode::Windowed => None, + }; + + if winit_window.fullscreen() != new_mode { + winit_window.set_fullscreen(new_mode); + } + } + if window.resolution != previous.resolution { + let physical_size = PhysicalSize::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ); + // Prevents "window.resolution values set from a winit resize event" from + // being set here, creating feedback loops. + if physical_size != info.last_winit_size { + 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.icon != previous.cursor.icon { + winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon)); + } + + if window.cursor.grab_mode != previous.cursor.grab_mode { + crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode); + } + + if window.cursor.visible != previous.cursor.visible { + winit_window.set_cursor_visible(window.cursor.visible); + } + + if window.cursor.hit_test != previous.cursor.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { + window.cursor.hit_test = previous.cursor.hit_test; + warn!( + "Could not set cursor hit test for window {:?}: {:?}", + window.title, err + ); + } + } + + if window.decorations != previous.decorations + && window.decorations != winit_window.is_decorated() + { + winit_window.set_decorations(window.decorations); + } + + if window.resizable != previous.resizable + && window.resizable != winit_window.is_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.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(), + ) { + let should_set = match winit_window.outer_position() { + Ok(current_position) => current_position != position, + _ => true, + }; + + if should_set { + winit_window.set_outer_position(position); + } + } + } + + if let Some(maximized) = window.internal.take_maximize_request() { + winit_window.set_maximized(maximized); + } + + if let Some(minimized) = window.internal.take_minimize_request() { + winit_window.set_minimized(minimized); + } + + 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); + } + + // 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." + ); + } + + info.previous = 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..2f7dda6702f7b 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,20 +1,18 @@ -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 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::{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,45 +23,40 @@ impl WinitWindows { pub fn create_window( &mut self, event_loop: &winit::event_loop::EventLoopWindowTarget<()>, - window_id: WindowId, - window_descriptor: &WindowDescriptor, - ) -> Window { + entity: Entity, + window: &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 window.mode { + WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some( + winit::window::Fullscreen::Borderless(event_loop.primary_monitor()), )), + WindowMode::Fullscreen => { + winit_window_builder.with_fullscreen(Some(winit::window::Fullscreen::Exclusive( + get_best_videomode(&event_loop.primary_monitor().unwrap()), + ))) + } 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(), + window.width() as u32, + window.height() as u32, )), )), WindowMode::Windowed => { - if let Some(sf) = scale_factor_override { + if let Some(position) = winit_window_position( + &window.position, + &window.resolution, + event_loop.available_monitors(), + event_loop.primary_monitor(), + None, + ) { + winit_window_builder = winit_window_builder.with_position(position); + } + + let logical_size = LogicalSize::new(window.width(), window.height()); + if let Some(sf) = window.resolution.scale_factor_override() { winit_window_builder.with_inner_size(logical_size.to_physical::(sf)) } else { winit_window_builder.with_inner_size(logical_size) @@ -72,12 +65,12 @@ impl WinitWindows { }; 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_always_on_top(window.always_on_top) + .with_resizable(window.resizable) + .with_decorations(window.decorations) + .with_transparent(window.transparent); - let constraints = window_descriptor.resize_constraints.check_constraints(); + let constraints = window.resize_constraints.check_constraints(); let min_inner_size = LogicalSize { width: constraints.min_width, height: constraints.min_height, @@ -97,14 +90,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(window.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) = &window.canvas { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let canvas = document @@ -121,59 +114,21 @@ 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)); - } - } - } - } + // Do not set the grab mode on window creation if it's none, this can fail on mobile + if window.cursor.grab_mode != CursorGrabMode::None { + attempt_grab(&winit_window, window.cursor.grab_mode); } - winit_window.set_cursor_visible(window_descriptor.cursor_visible); + winit_window.set_cursor_visible(window.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 window.canvas.is_none() { let canvas = winit_window.canvas(); let window = web_sys::window().unwrap(); @@ -185,45 +140,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 +219,89 @@ pub fn get_best_videomode(monitor: &winit::monitor::MonitorHandle) -> winit::mon modes.first().unwrap().clone() } + +pub(crate) fn attempt_grab(winit_window: &winit::window::Window, grab_mode: CursorGrabMode) { + let grab_result = match 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 grab_mode { + bevy_window::CursorGrabMode::Confined | bevy_window::CursorGrabMode::Locked => "grab", + bevy_window::CursorGrabMode::None => "ungrab", + }; + + bevy_utils::tracing::error!("Unable to {} cursor: {}", err_desc, err); + } +} + +// Ideally we could generify this across window backends, but we only really have winit atm +// so whatever. +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.cast::()) + } else { + warn!("Couldn't get monitor selected with: {monitor_selection:?}"); + None + } + } + WindowPosition::At(position) => { + Some(PhysicalPosition::new(position[0] as f64, position[1] as f64).cast::()) + } + } +} + +// 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 f1fc07b50c5c3..e5f086dd293ca 100644 --- a/examples/app/return_after_run.rs +++ b/examples/app/return_after_run.rs @@ -11,7 +11,7 @@ //! - //! - -use bevy::{prelude::*, winit::WinitSettings}; +use bevy::{prelude::*, window::WindowPlugin, winit::WinitSettings}; fn main() { println!("Running Bevy App"); @@ -21,10 +21,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..8053bd2e6a44d 100644 --- a/examples/ecs/parallel_query.rs +++ b/examples/ecs/parallel_query.rs @@ -37,8 +37,8 @@ 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(); +fn bounce_system(windows: Query<&Window>, mut sprites: Query<(&Transform, &mut Velocity)>) { + let window = windows.single(); let width = window.width(); let height = window.height(); let left = width / -2.0; diff --git a/examples/games/contributors.rs b/examples/games/contributors.rs index 8f452112ac667..86afff1822eae 100644 --- a/examples/games/contributors.rs +++ b/examples/games/contributors.rs @@ -245,17 +245,15 @@ fn velocity_system(time: Res