Windows as Entities (#5589)

# Objective

Fix https://github.com/bevyengine/bevy/issues/4530

- Make it easier to open/close/modify windows by setting them up as `Entity`s with a `Window` component.
- Make multiple windows very simple to set up. (just add a `Window` component to an entity and it should open)

## Solution

- Move all properties of window descriptor to ~components~ a component.
- Replace `WindowId` with `Entity`.
- ~Use change detection for components to update backend rather than events/commands. (The `CursorMoved`/`WindowResized`/... events are kept for user convenience.~
  Check each field individually to see what we need to update, events are still kept for user convenience.

---

## Changelog

- `WindowDescriptor` renamed to `Window`.
    - Width/height consolidated into a `WindowResolution` component.
    - Requesting maximization/minimization is done on the [`Window::state`] field.
- `WindowId` is now `Entity`.

## Migration Guide

- Replace `WindowDescriptor` with `Window`.
    - Change `width` and `height` fields in a `WindowResolution`, either by doing
      ```rust
      WindowResolution::new(width, height) // Explicitly
      // or using From<_> for tuples for convenience
      (1920., 1080.).into()
      ```
- Replace any `WindowCommand` code to just modify the `Window`'s fields directly  and creating/closing windows is now by spawning/despawning an entity with a `Window` component like so:
  ```rust
  let window = commands.spawn(Window { ... }).id(); // open window
  commands.entity(window).despawn(); // close window
  ```

## Unresolved
- ~How do we tell when a window is minimized by a user?~
  ~Currently using the `Resize(0, 0)` as an indicator of minimization.~
  No longer attempting to tell given how finnicky this was across platforms, now the user can only request that a window be maximized/minimized.
  
 ## Future work
 - Move `exit_on_close` functionality out from windowing and into app(?)
 - https://github.com/bevyengine/bevy/issues/5621
 - https://github.com/bevyengine/bevy/issues/7099
 - https://github.com/bevyengine/bevy/issues/7098


Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Aceeri 2023-01-19 00:38:28 +00:00
parent f0c504947c
commit ddfafab971
47 changed files with 1940 additions and 1892 deletions

View file

@ -13,6 +13,7 @@ use bevy_ecs::{
component::Component, component::Component,
entity::Entity, entity::Entity,
event::EventReader, event::EventReader,
prelude::With,
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Commands, Query, Res}, system::{Commands, Query, Res},
}; };
@ -21,7 +22,10 @@ use bevy_reflect::prelude::*;
use bevy_reflect::FromReflect; use bevy_reflect::FromReflect;
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;
use bevy_utils::HashSet; 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 std::{borrow::Cow, ops::Range};
use wgpu::{Extent3d, TextureFormat}; 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) /// The "target" that a [`Camera`] will render to. For example, this could be a [`Window`](bevy_window::Window)
/// swapchain or an [`Image`]. /// swapchain or an [`Image`].
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Reflect)]
pub enum RenderTarget { pub enum RenderTarget {
/// Window to which the camera's view is rendered. /// Window to which the camera's view is rendered.
Window(WindowId), Window(WindowRef),
/// Image to which the camera's view is rendered.
Image(Handle<Image>),
}
/// 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 to which the camera's view is rendered.
Image(Handle<Image>), Image(Handle<Image>),
} }
@ -340,16 +355,28 @@ impl Default for RenderTarget {
} }
impl 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<Entity>) -> Option<NormalizedRenderTarget> {
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>( pub fn get_texture_view<'a>(
&self, &self,
windows: &'a ExtractedWindows, windows: &'a ExtractedWindows,
images: &'a RenderAssets<Image>, images: &'a RenderAssets<Image>,
) -> Option<&'a TextureView> { ) -> Option<&'a TextureView> {
match self { match self {
RenderTarget::Window(window_id) => windows NormalizedRenderTarget::Window(window_ref) => windows
.get(window_id) .get(&window_ref.entity())
.and_then(|window| window.swap_chain_texture.as_ref()), .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) images.get(image_handle).map(|image| &image.texture_view)
} }
} }
@ -362,47 +389,55 @@ impl RenderTarget {
images: &'a RenderAssets<Image>, images: &'a RenderAssets<Image>,
) -> Option<TextureFormat> { ) -> Option<TextureFormat> {
match self { match self {
RenderTarget::Window(window_id) => windows NormalizedRenderTarget::Window(window_ref) => windows
.get(window_id) .get(&window_ref.entity())
.and_then(|window| window.swap_chain_texture_format), .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) images.get(image_handle).map(|image| image.texture_format)
} }
} }
} }
pub fn get_render_target_info( pub fn get_render_target_info<'a>(
&self, &self,
windows: &Windows, resolutions: impl IntoIterator<Item = (Entity, &'a Window)>,
images: &Assets<Image>, images: &Assets<Image>,
) -> Option<RenderTargetInfo> { ) -> Option<RenderTargetInfo> {
Some(match self { match self {
RenderTarget::Window(window_id) => { NormalizedRenderTarget::Window(window_ref) => resolutions
let window = windows.get(*window_id)?; .into_iter()
RenderTargetInfo { .find(|(entity, _)| *entity == window_ref.entity())
physical_size: UVec2::new(window.physical_width(), window.physical_height()), .map(|(_, window)| RenderTargetInfo {
scale_factor: window.scale_factor(), physical_size: UVec2::new(
} window.resolution.physical_width(),
} window.resolution.physical_height(),
RenderTarget::Image(image_handle) => { ),
scale_factor: window.resolution.scale_factor(),
}),
NormalizedRenderTarget::Image(image_handle) => {
let image = images.get(image_handle)?; let image = images.get(image_handle)?;
let Extent3d { width, height, .. } = image.texture_descriptor.size; let Extent3d { width, height, .. } = image.texture_descriptor.size;
RenderTargetInfo { Some(RenderTargetInfo {
physical_size: UVec2::new(width, height), physical_size: UVec2::new(width, height),
scale_factor: 1.0, scale_factor: 1.0,
} })
} }
}) }
} }
// Check if this render target is contained in the given changed windows or images. // Check if this render target is contained in the given changed windows or images.
fn is_changed( fn is_changed(
&self, &self,
changed_window_ids: &[WindowId], changed_window_ids: &HashSet<Entity>,
changed_image_handles: &HashSet<&Handle<Image>>, changed_image_handles: &HashSet<&Handle<Image>>,
) -> bool { ) -> bool {
match self { match self {
RenderTarget::Window(window_id) => changed_window_ids.contains(window_id), NormalizedRenderTarget::Window(window_ref) => {
RenderTarget::Image(image_handle) => changed_image_handles.contains(&image_handle), 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<T: CameraProjection + Component>(
mut window_resized_events: EventReader<WindowResized>, mut window_resized_events: EventReader<WindowResized>,
mut window_created_events: EventReader<WindowCreated>, mut window_created_events: EventReader<WindowCreated>,
mut image_asset_events: EventReader<AssetEvent<Image>>, mut image_asset_events: EventReader<AssetEvent<Image>>,
windows: Res<Windows>, primary_window: Query<Entity, With<PrimaryWindow>>,
windows: Query<(Entity, &Window)>,
images: Res<Assets<Image>>, images: Res<Assets<Image>>,
mut cameras: Query<(&mut Camera, &mut T)>, mut cameras: Query<(&mut Camera, &mut T)>,
) { ) {
let mut changed_window_ids = Vec::new(); let primary_window = primary_window.iter().next();
// Collect all unique window IDs of changed windows by inspecting created windows let mut changed_window_ids = HashSet::new();
for event in window_created_events.iter() { changed_window_ids.extend(window_created_events.iter().map(|event| event.window));
if changed_window_ids.contains(&event.id) { changed_window_ids.extend(window_resized_events.iter().map(|event| event.window));
continue;
}
changed_window_ids.push(event.id);
}
// 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 changed_image_handles: HashSet<&Handle<Image>> = image_asset_events let changed_image_handles: HashSet<&Handle<Image>> = image_asset_events
.iter() .iter()
@ -472,18 +494,18 @@ pub fn camera_system<T: CameraProjection + Component>(
.as_ref() .as_ref()
.map(|viewport| viewport.physical_size); .map(|viewport| viewport.physical_size);
if camera if let Some(normalized_target) = camera.target.normalize(primary_window) {
.target if normalized_target.is_changed(&changed_window_ids, &changed_image_handles)
.is_changed(&changed_window_ids, &changed_image_handles) || camera.is_added()
|| camera.is_added() || camera_projection.is_changed()
|| camera_projection.is_changed() || camera.computed.old_viewport_size != viewport_size
|| camera.computed.old_viewport_size != viewport_size {
{ camera.computed.target_info =
camera.computed.target_info = camera.target.get_render_target_info(&windows, &images); normalized_target.get_render_target_info(&windows, &images);
camera.computed.old_viewport_size = viewport_size; if let Some(size) = camera.logical_viewport_size() {
if let Some(size) = camera.logical_viewport_size() { camera_projection.update(size.x, size.y);
camera_projection.update(size.x, size.y); camera.computed.projection_matrix = camera_projection.get_projection_matrix();
camera.computed.projection_matrix = camera_projection.get_projection_matrix(); }
} }
} }
} }
@ -491,7 +513,7 @@ pub fn camera_system<T: CameraProjection + Component>(
#[derive(Component, Debug)] #[derive(Component, Debug)]
pub struct ExtractedCamera { pub struct ExtractedCamera {
pub target: RenderTarget, pub target: Option<NormalizedRenderTarget>,
pub physical_viewport_size: Option<UVec2>, pub physical_viewport_size: Option<UVec2>,
pub physical_target_size: Option<UVec2>, pub physical_target_size: Option<UVec2>,
pub viewport: Option<Viewport>, pub viewport: Option<Viewport>,
@ -510,7 +532,9 @@ pub fn extract_cameras(
&VisibleEntities, &VisibleEntities,
)>, )>,
>, >,
primary_window: Extract<Query<Entity, With<PrimaryWindow>>>,
) { ) {
let primary_window = primary_window.iter().next();
for (entity, camera, camera_render_graph, transform, visible_entities) in query.iter() { for (entity, camera, camera_render_graph, transform, visible_entities) in query.iter() {
if !camera.is_active { if !camera.is_active {
continue; continue;
@ -525,7 +549,7 @@ pub fn extract_cameras(
} }
commands.get_or_spawn(entity).insert(( commands.get_or_spawn(entity).insert((
ExtractedCamera { ExtractedCamera {
target: camera.target.clone(), target: camera.target.normalize(primary_window),
viewport: camera.viewport.clone(), viewport: camera.viewport.clone(),
physical_viewport_size: Some(viewport_size), physical_viewport_size: Some(viewport_size),
physical_target_size: Some(target_size), physical_target_size: Some(target_size),

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
camera::{ExtractedCamera, RenderTarget}, camera::{ExtractedCamera, NormalizedRenderTarget},
render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue}, render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue},
renderer::RenderContext, renderer::RenderContext,
view::ExtractedWindows, view::ExtractedWindows,
@ -52,8 +52,8 @@ impl Node for CameraDriverNode {
} }
previous_order_target = Some(new_order_target); previous_order_target = Some(new_order_target);
if let Ok((_, camera)) = self.cameras.get_manual(world, entity) { if let Ok((_, camera)) = self.cameras.get_manual(world, entity) {
if let RenderTarget::Window(id) = camera.target { if let Some(NormalizedRenderTarget::Window(window_ref)) = camera.target {
camera_windows.insert(id); camera_windows.insert(window_ref.entity());
} }
graph graph
.run_sub_graph(camera.render_graph.clone(), vec![SlotValue::Entity(entity)])?; .run_sub_graph(camera.render_graph.clone(), vec![SlotValue::Entity(entity)])?;

View file

@ -37,6 +37,7 @@ pub mod prelude {
}; };
} }
use bevy_window::{PrimaryWindow, RawHandleWrapper};
use globals::GlobalsPlugin; use globals::GlobalsPlugin;
pub use once_cell; pub use once_cell;
@ -50,7 +51,7 @@ use crate::{
}; };
use bevy_app::{App, AppLabel, Plugin}; use bevy_app::{App, AppLabel, Plugin};
use bevy_asset::{AddAsset, AssetServer}; use bevy_asset::{AddAsset, AssetServer};
use bevy_ecs::prelude::*; use bevy_ecs::{prelude::*, system::SystemState};
use bevy_utils::tracing::debug; use bevy_utils::tracing::debug;
use std::{ use std::{
any::TypeId, any::TypeId,
@ -138,17 +139,17 @@ impl Plugin for RenderPlugin {
.init_asset_loader::<ShaderLoader>() .init_asset_loader::<ShaderLoader>()
.init_debug_asset_loader::<ShaderLoader>(); .init_debug_asset_loader::<ShaderLoader>();
if let Some(backends) = self.wgpu_settings.backends { let mut system_state: SystemState<Query<&RawHandleWrapper, With<PrimaryWindow>>> =
let windows = app.world.resource_mut::<bevy_window::Windows>(); SystemState::new(&mut app.world);
let instance = wgpu::Instance::new(backends); let primary_window = system_state.get(&app.world);
let surface = windows if let Some(backends) = self.wgpu_settings.backends {
.get_primary() let instance = wgpu::Instance::new(backends);
.and_then(|window| window.raw_handle()) let surface = primary_window.get_single().ok().map(|wrapper| unsafe {
.map(|wrapper| unsafe { // SAFETY: Plugins should be set up on the main thread.
let handle = wrapper.get_handle(); let handle = wrapper.get_handle();
instance.create_surface(&handle) instance.create_surface(&handle)
}); });
let request_adapter_options = wgpu::RequestAdapterOptions { let request_adapter_options = wgpu::RequestAdapterOptions {
power_preference: self.wgpu_settings.power_preference, power_preference: self.wgpu_settings.power_preference,

View file

@ -285,10 +285,10 @@ fn prepare_view_targets(
) { ) {
let mut textures = HashMap::default(); let mut textures = HashMap::default();
for (entity, camera, view) in cameras.iter() { 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)) = ( if let (Some(out_texture_view), Some(out_texture_format)) = (
camera.target.get_texture_view(&windows, &images), target.get_texture_view(&windows, &images),
camera.target.get_texture_format(&windows, &images), target.get_texture_format(&windows, &images),
) { ) {
let size = Extent3d { let size = Extent3d {
width: target_size.x, width: target_size.x,

View file

@ -7,7 +7,7 @@ use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_utils::{tracing::debug, HashMap, HashSet}; use bevy_utils::{tracing::debug, HashMap, HashSet};
use bevy_window::{ use bevy_window::{
CompositeAlphaMode, PresentMode, RawHandleWrapper, WindowClosed, WindowId, Windows, CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed,
}; };
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use wgpu::TextureFormat; use wgpu::TextureFormat;
@ -40,8 +40,9 @@ impl Plugin for WindowRenderPlugin {
} }
pub struct ExtractedWindow { pub struct ExtractedWindow {
pub id: WindowId, /// An entity that contains the components in [`Window`].
pub raw_handle: Option<RawHandleWrapper>, pub entity: Entity,
pub handle: RawHandleWrapper,
pub physical_width: u32, pub physical_width: u32,
pub physical_height: u32, pub physical_height: u32,
pub present_mode: PresentMode, pub present_mode: PresentMode,
@ -54,11 +55,12 @@ pub struct ExtractedWindow {
#[derive(Default, Resource)] #[derive(Default, Resource)]
pub struct ExtractedWindows { pub struct ExtractedWindows {
pub windows: HashMap<WindowId, ExtractedWindow>, pub primary: Option<Entity>,
pub windows: HashMap<Entity, ExtractedWindow>,
} }
impl Deref for ExtractedWindows { impl Deref for ExtractedWindows {
type Target = HashMap<WindowId, ExtractedWindow>; type Target = HashMap<Entity, ExtractedWindow>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.windows &self.windows
@ -74,36 +76,37 @@ impl DerefMut for ExtractedWindows {
fn extract_windows( fn extract_windows(
mut extracted_windows: ResMut<ExtractedWindows>, mut extracted_windows: ResMut<ExtractedWindows>,
mut closed: Extract<EventReader<WindowClosed>>, mut closed: Extract<EventReader<WindowClosed>>,
windows: Extract<Res<Windows>>, windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
) { ) {
for window in windows.iter() { for (entity, window, handle, primary) in windows.iter() {
let (new_width, new_height) = ( if primary.is_some() {
window.physical_width().max(1), extracted_windows.primary = Some(entity);
window.physical_height().max(1), }
);
let new_present_mode = window.present_mode();
let mut extracted_window = let (new_width, new_height) = (
extracted_windows window.resolution.physical_width().max(1),
.entry(window.id()) window.resolution.physical_height().max(1),
.or_insert(ExtractedWindow { );
id: window.id(),
raw_handle: window.raw_handle(), let mut extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow {
physical_width: new_width, entity,
physical_height: new_height, handle: handle.clone(),
present_mode: window.present_mode(), physical_width: new_width,
swap_chain_texture: None, physical_height: new_height,
swap_chain_texture_format: None, present_mode: window.present_mode,
size_changed: false, swap_chain_texture: None,
present_mode_changed: false, size_changed: false,
alpha_mode: window.alpha_mode(), swap_chain_texture_format: None,
}); present_mode_changed: false,
alpha_mode: window.composite_alpha_mode,
});
// NOTE: Drop the swap chain frame here // NOTE: Drop the swap chain frame here
extracted_window.swap_chain_texture = None; extracted_window.swap_chain_texture = None;
extracted_window.size_changed = new_width != extracted_window.physical_width extracted_window.size_changed = new_width != extracted_window.physical_width
|| new_height != extracted_window.physical_height; || 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 { if extracted_window.size_changed {
debug!( debug!(
@ -120,13 +123,14 @@ fn extract_windows(
if extracted_window.present_mode_changed { if extracted_window.present_mode_changed {
debug!( debug!(
"Window Present Mode changed from {:?} to {:?}", "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() { 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)] #[derive(Resource, Default)]
pub struct WindowSurfaces { pub struct WindowSurfaces {
surfaces: HashMap<WindowId, SurfaceData>, surfaces: HashMap<Entity, SurfaceData>,
/// List of windows that we have already called the initial `configure_surface` for /// List of windows that we have already called the initial `configure_surface` for
configured_windows: HashSet<WindowId>, configured_windows: HashSet<Entity>,
} }
/// Creates and (re)configures window surfaces, and obtains a swapchain texture for rendering. /// Creates and (re)configures window surfaces, and obtains a swapchain texture for rendering.
@ -173,20 +177,14 @@ pub fn prepare_windows(
render_instance: Res<RenderInstance>, render_instance: Res<RenderInstance>,
render_adapter: Res<RenderAdapter>, render_adapter: Res<RenderAdapter>,
) { ) {
for window in windows for window in windows.windows.values_mut() {
.windows
.values_mut()
// value of raw_handle is only None in synthetic tests
.filter(|x| x.raw_handle.is_some())
{
let window_surfaces = window_surfaces.deref_mut(); let window_surfaces = window_surfaces.deref_mut();
let surface_data = window_surfaces let surface_data = window_surfaces
.surfaces .surfaces
.entry(window.id) .entry(window.entity)
.or_insert_with(|| unsafe { .or_insert_with(|| unsafe {
// NOTE: On some OSes this MUST be called from the main thread. // NOTE: On some OSes this MUST be called from the main thread.
let surface = render_instance let surface = render_instance.create_surface(&window.handle.get_handle());
.create_surface(&window.raw_handle.as_ref().unwrap().get_handle());
let format = *surface let format = *surface
.get_supported_formats(&render_adapter) .get_supported_formats(&render_adapter)
.get(0) .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; let surface = &surface_data.surface;
if not_already_configured || window.size_changed || window.present_mode_changed { if not_already_configured || window.size_changed || window.present_mode_changed {

View file

@ -5,6 +5,7 @@ use bevy_ecs::{
component::Component, component::Component,
entity::Entity, entity::Entity,
event::EventReader, event::EventReader,
prelude::With,
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Commands, Local, Query, Res, ResMut}, system::{Commands, Local, Query, Res, ResMut},
}; };
@ -19,7 +20,7 @@ use bevy_render::{
use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlas}; use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlas};
use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_transform::prelude::{GlobalTransform, Transform};
use bevy_utils::HashSet; use bevy_utils::HashSet;
use bevy_window::{WindowId, WindowScaleFactorChanged, Windows}; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged};
use crate::{ use crate::{
Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline,
@ -69,7 +70,7 @@ pub struct Text2dBundle {
pub fn extract_text2d_sprite( pub fn extract_text2d_sprite(
mut extracted_sprites: ResMut<ExtractedSprites>, mut extracted_sprites: ResMut<ExtractedSprites>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>, texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
windows: Extract<Res<Windows>>, windows: Extract<Query<&Window, With<PrimaryWindow>>>,
text2d_query: Extract< text2d_query: Extract<
Query<( Query<(
Entity, 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 for (entity, computed_visibility, text, text_layout_info, anchor, text_transform) in
text2d_query.iter() text2d_query.iter()
@ -146,9 +151,9 @@ pub fn update_text2d_layout(
mut queue: Local<HashSet<Entity>>, mut queue: Local<HashSet<Entity>>,
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
windows: Res<Windows>,
text_settings: Res<TextSettings>, text_settings: Res<TextSettings>,
mut font_atlas_warning: ResMut<FontAtlasWarning>, mut font_atlas_warning: ResMut<FontAtlasWarning>,
windows: Query<&Window, With<PrimaryWindow>>,
mut scale_factor_changed: EventReader<WindowScaleFactorChanged>, mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>, mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>, mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
@ -162,7 +167,12 @@ pub fn update_text2d_layout(
) { ) {
// We need to consume the entire iterator, hence `last` // We need to consume the entire iterator, hence `last`
let factor_changed = scale_factor_changed.iter().last().is_some(); 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 { for (entity, text, bounds, text_layout_info) in &mut text_query {
if factor_changed || text.is_changed() || queue.remove(&entity) { if factor_changed || text.is_changed() || queue.remove(&entity) {

View file

@ -13,7 +13,7 @@ use bevy_log::warn;
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
use bevy_utils::HashMap; use bevy_utils::HashMap;
use bevy_window::{Window, WindowId, WindowScaleFactorChanged, Windows}; use bevy_window::{PrimaryWindow, Window, WindowResolution, WindowScaleFactorChanged};
use std::fmt; use std::fmt;
use taffy::{ use taffy::{
prelude::{AvailableSpace, Size}, prelude::{AvailableSpace, Size},
@ -23,7 +23,7 @@ use taffy::{
#[derive(Resource)] #[derive(Resource)]
pub struct FlexSurface { pub struct FlexSurface {
entity_to_taffy: HashMap<Entity, taffy::node::Node>, entity_to_taffy: HashMap<Entity, taffy::node::Node>,
window_nodes: HashMap<WindowId, taffy::node::Node>, window_nodes: HashMap<Entity, taffy::node::Node>,
taffy: Taffy, taffy: Taffy,
} }
@ -36,7 +36,6 @@ unsafe impl Sync for FlexSurface {}
fn _assert_send_sync_flex_surface_impl_safe() { fn _assert_send_sync_flex_surface_impl_safe() {
fn _assert_send_sync<T: Send + Sync>() {} fn _assert_send_sync<T: Send + Sync>() {}
_assert_send_sync::<HashMap<Entity, taffy::node::Node>>(); _assert_send_sync::<HashMap<Entity, taffy::node::Node>>();
_assert_send_sync::<HashMap<WindowId, taffy::node::Node>>();
// FIXME https://github.com/DioxusLabs/taffy/issues/146 // FIXME https://github.com/DioxusLabs/taffy/issues/146
// _assert_send_sync::<Taffy>(); // _assert_send_sync::<Taffy>();
} }
@ -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 taffy = &mut self.taffy;
let node = self let node = self
.window_nodes .window_nodes
.entry(window.id()) .entry(window)
.or_insert_with(|| taffy.new_leaf(taffy::style::Style::default()).unwrap()); .or_insert_with(|| taffy.new_leaf(taffy::style::Style::default()).unwrap());
taffy taffy
@ -157,8 +156,12 @@ without UI components as a child of an entity with UI components, results may be
*node, *node,
taffy::style::Style { taffy::style::Style {
size: taffy::geometry::Size { size: taffy::geometry::Size {
width: taffy::style::Dimension::Points(window.physical_width() as f32), width: taffy::style::Dimension::Points(
height: taffy::style::Dimension::Points(window.physical_height() as f32), window_resolution.physical_width() as f32
),
height: taffy::style::Dimension::Points(
window_resolution.physical_height() as f32,
),
}, },
..Default::default() ..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( pub fn set_window_children(
&mut self, &mut self,
window_id: WindowId, parent_window: Entity,
children: impl Iterator<Item = Entity>, children: impl Iterator<Item = Entity>,
) { ) {
let taffy_node = self.window_nodes.get(&window_id).unwrap(); let taffy_node = self.window_nodes.get(&parent_window).unwrap();
let child_nodes = children let child_nodes = children
.map(|e| *self.entity_to_taffy.get(&e).unwrap()) .map(|e| *self.entity_to_taffy.get(&e).unwrap())
.collect::<Vec<taffy::node::Node>>(); .collect::<Vec<taffy::node::Node>>();
@ -218,7 +221,8 @@ pub enum FlexError {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn flex_node_system( pub fn flex_node_system(
windows: Res<Windows>, primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
windows: Query<(Entity, &Window)>,
ui_scale: Res<UiScale>, ui_scale: Res<UiScale>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>, mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut flex_surface: ResMut<FlexSurface>, mut flex_surface: ResMut<FlexSurface>,
@ -234,13 +238,20 @@ pub fn flex_node_system(
mut node_transform_query: Query<(Entity, &mut Node, &mut Transform, Option<&Parent>)>, mut node_transform_query: Query<(Entity, &mut Node, &mut Transform, Option<&Parent>)>,
removed_nodes: RemovedComponents<Node>, removed_nodes: RemovedComponents<Node>,
) { ) {
// 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 // update window root nodes
for window in windows.iter() { for (entity, window) in windows.iter() {
flex_surface.update_window(window); 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; let scale_factor = logical_to_physical_factor * ui_scale.scale;
fn update_changed<F: ReadOnlyWorldQuery>( fn update_changed<F: ReadOnlyWorldQuery>(
@ -273,9 +284,7 @@ pub fn flex_node_system(
flex_surface.remove_entities(&removed_nodes); flex_surface.remove_entities(&removed_nodes);
// update window children (for now assuming all Nodes live in the primary window) // 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_entity, root_node_query.iter());
flex_surface.set_window_children(primary_window.id(), root_node_query.iter());
}
// update and remove children // update and remove children
for entity in &removed_children { for entity in &removed_children {

View file

@ -3,7 +3,7 @@ use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
change_detection::DetectChangesMut, change_detection::DetectChangesMut,
entity::Entity, entity::Entity,
prelude::Component, prelude::{Component, With},
query::WorldQuery, query::WorldQuery,
reflect::ReflectComponent, reflect::ReflectComponent,
system::{Local, Query, Res}, system::{Local, Query, Res},
@ -11,10 +11,10 @@ use bevy_ecs::{
use bevy_input::{mouse::MouseButton, touch::Touches, Input}; use bevy_input::{mouse::MouseButton, touch::Touches, Input};
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
use bevy_render::camera::{Camera, RenderTarget}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::ComputedVisibility};
use bevy_render::view::ComputedVisibility;
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;
use bevy_window::Windows;
use bevy_window::{PrimaryWindow, Window};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smallvec::SmallVec; 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 /// 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. /// Entities with a hidden [`ComputedVisibility`] are always treated as released.
#[allow(clippy::too_many_arguments)]
pub fn ui_focus_system( pub fn ui_focus_system(
mut state: Local<State>, mut state: Local<State>,
camera: Query<(&Camera, Option<&UiCameraConfig>)>, camera: Query<(&Camera, Option<&UiCameraConfig>)>,
windows: Res<Windows>, windows: Query<&Window>,
mouse_button_input: Res<Input<MouseButton>>, mouse_button_input: Res<Input<MouseButton>>,
touches_input: Res<Touches>, touches_input: Res<Touches>,
ui_stack: Res<UiStack>, ui_stack: Res<UiStack>,
mut node_query: Query<NodeQuery>, mut node_query: Query<NodeQuery>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) { ) {
let primary_window = primary_window.iter().next();
// reset entities that were both clicked and released in the last frame // reset entities that were both clicked and released in the last frame
for entity in state.entities_to_reset.drain(..) { for entity in state.entities_to_reset.drain(..) {
if let Ok(mut interaction) = node_query.get_component_mut::<Interaction>(entity) { if let Ok(mut interaction) = node_query.get_component_mut::<Interaction>(entity) {
@ -167,18 +171,20 @@ pub fn ui_focus_system(
.iter() .iter()
.filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui)) .filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui))
.filter_map(|(camera, _)| { .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) Some(window_id)
} else { } else {
None None
} }
}) })
.filter_map(|window_id| windows.get(window_id)) .find_map(|window_ref| {
.filter(|window| window.is_focused()) windows.get(window_ref.entity()).ok().and_then(|window| {
.find_map(|window| { window.cursor.position.map(|mut cursor_pos| {
window.cursor_position().map(|mut cursor_pos| { cursor_pos.y = window.height() as f64 - cursor_pos.y;
cursor_pos.y = window.height() - cursor_pos.y; cursor_pos.as_vec2()
cursor_pos })
}) })
}) })
.or_else(|| touches_input.first_pressed_position()); .or_else(|| touches_input.first_pressed_position());

View file

@ -2,6 +2,7 @@ mod pipeline;
mod render_pass; mod render_pass;
use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
use bevy_window::{PrimaryWindow, Window};
pub use pipeline::*; pub use pipeline::*;
pub use render_pass::*; pub use render_pass::*;
@ -29,7 +30,6 @@ use bevy_text::{Text, TextLayoutInfo};
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;
use bevy_utils::FloatOrd; use bevy_utils::FloatOrd;
use bevy_utils::HashMap; use bevy_utils::HashMap;
use bevy_window::{WindowId, Windows};
use bytemuck::{Pod, Zeroable}; use bytemuck::{Pod, Zeroable};
use std::ops::Range; use std::ops::Range;
@ -297,7 +297,7 @@ pub fn extract_default_ui_camera_view<T: Component>(
pub fn extract_text_uinodes( pub fn extract_text_uinodes(
mut extracted_uinodes: ResMut<ExtractedUiNodes>, mut extracted_uinodes: ResMut<ExtractedUiNodes>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>, texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
windows: Extract<Res<Windows>>, windows: Extract<Query<&Window, With<PrimaryWindow>>>,
ui_stack: Extract<Res<UiStack>>, ui_stack: Extract<Res<UiStack>>,
uinode_query: Extract< uinode_query: Extract<
Query<( 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() { for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() {
if let Ok((uinode, global_transform, text, text_layout_info, visibility, clip)) = if let Ok((uinode, global_transform, text, text_layout_info, visibility, clip)) =
uinode_query.get(*entity) uinode_query.get(*entity)

View file

@ -12,7 +12,7 @@ use bevy_text::{
Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline,
TextSettings, YAxisOrientation, TextSettings, YAxisOrientation,
}; };
use bevy_window::Windows; use bevy_window::{PrimaryWindow, Window};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct QueuedText { pub struct QueuedText {
@ -51,7 +51,7 @@ pub fn text_system(
mut last_scale_factor: Local<f64>, mut last_scale_factor: Local<f64>,
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
windows: Res<Windows>, windows: Query<&Window, With<PrimaryWindow>>,
text_settings: Res<TextSettings>, text_settings: Res<TextSettings>,
mut font_atlas_warning: ResMut<FontAtlasWarning>, mut font_atlas_warning: ResMut<FontAtlasWarning>,
ui_scale: Res<UiScale>, ui_scale: Res<UiScale>,
@ -69,13 +69,11 @@ pub fn text_system(
)>, )>,
)>, )>,
) { ) {
// TODO: This should support window-independent scale settings. // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
// See https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows
let scale_factor = if let Some(window) = windows.get_primary() { .get_single()
window.scale_factor() * ui_scale.scale .map(|window| window.resolution.scale_factor())
} else { .unwrap_or(ui_scale.scale);
ui_scale.scale
};
let inv_scale_factor = 1. / scale_factor; let inv_scale_factor = 1. / scale_factor;

View file

@ -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. /// 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). /// 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). /// 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. /// `winit`, in turn, mostly copied cursor types available in the browser.
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] #[derive(Default, Debug, Hash, PartialEq, Eq, Clone, Copy, Reflect, FromReflect)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[reflect(Debug, PartialEq, Default)]
pub enum CursorIcon { pub enum CursorIcon {
/// The platform-dependent default cursor. /// The platform-dependent default cursor.
#[default]
Default, Default,
/// A simple crosshair. /// A simple crosshair.
Crosshair, Crosshair,

View file

@ -1,6 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use super::{WindowDescriptor, WindowId}; use bevy_ecs::entity::Entity;
use bevy_math::{IVec2, Vec2}; use bevy_math::{IVec2, Vec2};
use bevy_reflect::{FromReflect, Reflect}; use bevy_reflect::{FromReflect, Reflect};
@ -16,26 +16,15 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowResized { pub struct WindowResized {
pub id: WindowId, /// Window that has changed.
pub window: Entity,
/// The new logical width of the window. /// The new logical width of the window.
pub width: f32, pub width: f32,
/// The new logical height of the window. /// The new logical height of the window.
pub height: f32, pub height: f32,
} }
/// An event that indicates that a new window should be created. // TODO: This would redraw all windows ? If yes, update docs to reflect this
#[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,
}
/// An event that indicates the window should redraw, even if its control flow is set to `Wait` and /// An event that indicates the window should redraw, even if its control flow is set to `Wait` and
/// there have been no window events. /// there have been no window events.
#[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[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. /// An event that is sent whenever a new window is created.
/// ///
/// To create a new window, send a [`CreateWindow`] event - this /// To create a new window, spawn an entity with a [`crate::Window`] on it.
/// event will be sent in the handler for that event.
#[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)]
#[reflect(Debug, PartialEq)] #[reflect(Debug, PartialEq)]
#[cfg_attr( #[cfg_attr(
@ -59,20 +47,20 @@ pub struct RequestRedraw;
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowCreated { 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 /// 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. /// 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 /// 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 disable this behaviour, set `close_when_requested` on the [`WindowPlugin`]
/// to `false`. /// to `false`.
/// ///
/// [`WindowPlugin`]: crate::WindowPlugin /// [`WindowPlugin`]: crate::WindowPlugin
/// [`Window`]: crate::Window /// [`Window`]: crate::Window
/// [closing]: crate::Window::close
#[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)]
#[reflect(Debug, PartialEq)] #[reflect(Debug, PartialEq)]
#[cfg_attr( #[cfg_attr(
@ -81,13 +69,12 @@ pub struct WindowCreated {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowCloseRequested { 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 /// An event that is sent whenever a window is closed. This will be sent when
/// handler for [`Window::close`]. /// the window entity loses its `Window` component or is despawned.
///
/// [`Window::close`]: crate::Window::close
#[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)]
#[reflect(Debug, PartialEq)] #[reflect(Debug, PartialEq)]
#[cfg_attr( #[cfg_attr(
@ -96,10 +83,13 @@ pub struct WindowCloseRequested {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowClosed { 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 inside a window.
/// An event reporting that the mouse cursor has moved on a window.
/// ///
/// The event is sent only if the cursor is over one of the application's windows. /// 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. /// It is the translated version of [`WindowEvent::CursorMoved`] from the `winit` crate.
@ -116,10 +106,9 @@ pub struct WindowClosed {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct CursorMoved { pub struct CursorMoved {
/// The identifier of the window the cursor has moved on. /// Window that the cursor moved inside.
pub id: WindowId, pub window: Entity,
/// The cursor position in logical pixels.
/// The position of the cursor, in window coordinates.
pub position: Vec2, pub position: Vec2,
} }
@ -132,7 +121,8 @@ pub struct CursorMoved {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct CursorEntered { 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. /// An event that is sent whenever the user's cursor leaves a window.
@ -144,7 +134,8 @@ pub struct CursorEntered {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct CursorLeft { 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. /// 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) reflect(Serialize, Deserialize)
)] )]
pub struct ReceivedCharacter { pub struct ReceivedCharacter {
pub id: WindowId, /// Window that received the character.
pub window: Entity,
/// Received character.
pub char: char, pub char: char,
} }
@ -169,7 +162,9 @@ pub struct ReceivedCharacter {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowFocused { 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, pub focused: bool,
} }
@ -182,7 +177,9 @@ pub struct WindowFocused {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowScaleFactorChanged { 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, pub scale_factor: f64,
} }
@ -195,7 +192,9 @@ pub struct WindowScaleFactorChanged {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowBackendScaleFactorChanged { 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, pub scale_factor: f64,
} }
@ -208,11 +207,27 @@ pub struct WindowBackendScaleFactorChanged {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub enum FileDragAndDrop { pub enum FileDragAndDrop {
DroppedFile { id: WindowId, path_buf: PathBuf }, /// 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,
},
HoveredFile { id: WindowId, 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,
},
HoveredFileCancelled { id: WindowId }, /// 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. /// An event that is sent when a window is repositioned in physical pixels.
@ -224,6 +239,8 @@ pub enum FileDragAndDrop {
reflect(Serialize, Deserialize) reflect(Serialize, Deserialize)
)] )]
pub struct WindowMoved { pub struct WindowMoved {
pub id: WindowId, /// Window that moved.
pub entity: Entity,
/// Where the window moved to in physical pixels.
pub position: IVec2, pub position: IVec2,
} }

View file

@ -4,34 +4,33 @@ mod event;
mod raw_handle; mod raw_handle;
mod system; mod system;
mod window; mod window;
mod windows;
pub use crate::raw_handle::*; pub use crate::raw_handle::*;
pub use cursor::*; pub use cursor::*;
pub use event::*; pub use event::*;
pub use system::*; pub use system::*;
pub use window::*; pub use window::*;
pub use windows::*;
pub mod prelude { pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
pub use crate::{ pub use crate::{
CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, MonitorSelection, CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, MonitorSelection,
ReceivedCharacter, Window, WindowDescriptor, WindowMode, WindowMoved, WindowPlugin, ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition,
WindowPosition, Windows, WindowResizeConstraints,
}; };
} }
use bevy_app::prelude::*;
use bevy_ecs::schedule::{IntoSystemDescriptor, SystemLabel};
use std::path::PathBuf; use std::path::PathBuf;
use bevy_app::prelude::*;
use bevy_ecs::schedule::SystemLabel;
impl Default for WindowPlugin { impl Default for WindowPlugin {
fn default() -> Self { fn default() -> Self {
WindowPlugin { WindowPlugin {
window: Default::default(), primary_window: Some(Window::default()),
add_primary_window: true, exit_condition: ExitCondition::OnAllClosed,
exit_on_all_closed: true,
close_when_requested: true, close_when_requested: true,
} }
} }
@ -39,21 +38,26 @@ impl Default for WindowPlugin {
/// A [`Plugin`] that defines an interface for windowing support in Bevy. /// A [`Plugin`] that defines an interface for windowing support in Bevy.
pub struct WindowPlugin { pub struct WindowPlugin {
pub window: WindowDescriptor, /// Settings for the primary window. This will be spawned by
/// Whether to create a window when added. /// 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, /// Note that if there are no windows, by default the App will exit,
/// due to [`exit_on_all_closed`]. /// due to [`exit_on_all_closed`].
pub add_primary_window: bool, pub primary_window: Option<Window>,
/// Whether to exit the app when there are no open windows. /// Whether to exit the app when there are no open windows.
/// ///
/// If disabling this, ensure that you send the [`bevy_app::AppExit`] /// If disabling this, ensure that you send the [`bevy_app::AppExit`]
/// event when the app should exit. If this does not occur, you will /// event when the app should exit. If this does not occur, you will
/// create 'headless' processes (processes without windows), which may /// 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`]. /// [`ExitCondition::OnAllClosed`] will add [`exit_on_all_closed`] to [`CoreStage::Update`].
pub exit_on_all_closed: bool, /// [`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. /// Whether to close windows when they are requested to be closed (i.e.
/// when the close button is pressed). /// when the close button is pressed).
/// ///
@ -65,8 +69,8 @@ pub struct WindowPlugin {
impl Plugin for WindowPlugin { impl Plugin for WindowPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// User convenience events
app.add_event::<WindowResized>() app.add_event::<WindowResized>()
.add_event::<CreateWindow>()
.add_event::<WindowCreated>() .add_event::<WindowCreated>()
.add_event::<WindowClosed>() .add_event::<WindowClosed>()
.add_event::<WindowCloseRequested>() .add_event::<WindowCloseRequested>()
@ -79,29 +83,30 @@ impl Plugin for WindowPlugin {
.add_event::<WindowScaleFactorChanged>() .add_event::<WindowScaleFactorChanged>()
.add_event::<WindowBackendScaleFactorChanged>() .add_event::<WindowBackendScaleFactorChanged>()
.add_event::<FileDragAndDrop>() .add_event::<FileDragAndDrop>()
.add_event::<WindowMoved>() .add_event::<WindowMoved>();
.init_resource::<Windows>();
if self.add_primary_window { if let Some(primary_window) = &self.primary_window {
app.world.send_event(CreateWindow { app.world
id: WindowId::primary(), .spawn(primary_window.clone())
descriptor: self.window.clone(), .insert(PrimaryWindow);
});
} }
if self.exit_on_all_closed { match self.exit_condition {
app.add_system_to_stage( ExitCondition::OnPrimaryClosed => {
CoreStage::PostUpdate, app.add_system(exit_on_primary_closed);
exit_on_all_closed.after(ModifiesWindows), }
); ExitCondition::OnAllClosed => {
app.add_system(exit_on_all_closed);
}
ExitCondition::DontExit => {}
} }
if self.close_when_requested { if self.close_when_requested {
app.add_system(close_when_requested); app.add_system_to_stage(CoreStage::First, close_when_requested);
} }
// Register event types // Register event types
app.register_type::<WindowResized>() app.register_type::<WindowResized>()
.register_type::<CreateWindow>()
.register_type::<RequestRedraw>() .register_type::<RequestRedraw>()
.register_type::<WindowCreated>() .register_type::<WindowCreated>()
.register_type::<WindowCloseRequested>() .register_type::<WindowCloseRequested>()
@ -117,18 +122,41 @@ impl Plugin for WindowPlugin {
.register_type::<WindowMoved>(); .register_type::<WindowMoved>();
// Register window descriptor and related types // Register window descriptor and related types
app.register_type::<WindowId>() app.register_type::<Window>()
.register_type::<PresentMode>() .register_type::<Cursor>()
.register_type::<WindowResizeConstraints>() .register_type::<WindowResolution>()
.register_type::<WindowMode>()
.register_type::<WindowPosition>() .register_type::<WindowPosition>()
.register_type::<WindowMode>()
.register_type::<PresentMode>()
.register_type::<InternalWindowState>()
.register_type::<MonitorSelection>() .register_type::<MonitorSelection>()
.register_type::<WindowDescriptor>(); .register_type::<WindowResizeConstraints>();
// Register `PathBuf` as it's used by `FileDragAndDrop` // Register `PathBuf` as it's used by `FileDragAndDrop`
app.register_type::<PathBuf>(); app.register_type::<PathBuf>();
} }
} }
/// System Label marking when changes are applied to windows
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub struct ModifiesWindows; 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,
}

View file

@ -1,3 +1,4 @@
use bevy_ecs::prelude::Component;
use raw_window_handle::{ use raw_window_handle::{
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, 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, /// 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`]) /// and so we cannot simply make it (or any type that has a safe operation to get a [`RawWindowHandle`] or [`RawDisplayHandle`])
/// thread-safe. /// thread-safe.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Component)]
pub struct RawHandleWrapper { pub struct RawHandleWrapper {
pub window_handle: RawWindowHandle, pub window_handle: RawWindowHandle,
pub display_handle: RawDisplayHandle, pub display_handle: RawDisplayHandle,

View file

@ -1,4 +1,4 @@
use crate::{Window, WindowCloseRequested, Windows}; use crate::{PrimaryWindow, Window, WindowCloseRequested};
use bevy_app::AppExit; use bevy_app::AppExit;
use bevy_ecs::prelude::*; 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. /// Ensure that you read the caveats documented on that field if doing so.
/// ///
/// [`WindowPlugin`]: crate::WindowPlugin /// [`WindowPlugin`]: crate::WindowPlugin
pub fn exit_on_all_closed(mut app_exit_events: EventWriter<AppExit>, windows: Res<Windows>) { pub fn exit_on_all_closed(mut app_exit_events: EventWriter<AppExit>, windows: Query<&Window>) {
if windows.iter().count() == 0 { 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<AppExit>,
windows: Query<(), (With<Window>, With<PrimaryWindow>)>,
) {
if windows.is_empty() {
bevy_utils::tracing::info!("Primary windows was closed, exiting");
app_exit_events.send(AppExit); app_exit_events.send(AppExit);
} }
} }
@ -24,22 +40,27 @@ pub fn exit_on_all_closed(mut app_exit_events: EventWriter<AppExit>, windows: Re
/// Ensure that you read the caveats documented on that field if doing so. /// Ensure that you read the caveats documented on that field if doing so.
/// ///
/// [`WindowPlugin`]: crate::WindowPlugin /// [`WindowPlugin`]: crate::WindowPlugin
pub fn close_when_requested( pub fn close_when_requested(mut commands: Commands, mut closed: EventReader<WindowCloseRequested>) {
mut windows: ResMut<Windows>,
mut closed: EventReader<WindowCloseRequested>,
) {
for event in closed.iter() { 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 (<kbd>Esc</kbd>) is pressed /// Close the focused window whenever the escape key (<kbd>Esc</kbd>) is pressed
/// ///
/// This is useful for examples or prototyping. /// This is useful for examples or prototyping.
pub fn close_on_esc(mut windows: ResMut<Windows>, input: Res<Input<KeyCode>>) { pub fn close_on_esc(
if input.just_pressed(KeyCode::Escape) { mut commands: Commands,
if let Some(window) = windows.get_focused_mut() { focused_windows: Query<(Entity, &Window)>,
window.close(); input: Res<Input<KeyCode>>,
) {
for (window, focus) in focused_windows.iter() {
if !focus.focused {
continue;
}
if input.just_pressed(KeyCode::Escape) {
commands.entity(window).despawn();
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,7 @@ pub fn convert_mouse_button(mouse_button: winit::event::MouseButton) -> MouseBut
pub fn convert_touch_input( pub fn convert_touch_input(
touch_input: winit::event::Touch, touch_input: winit::event::Touch,
location: winit::dpi::LogicalPosition<f32>, location: winit::dpi::LogicalPosition<f64>,
) -> TouchInput { ) -> TouchInput {
TouchInput { TouchInput {
phase: match touch_input.phase { phase: match touch_input.phase {
@ -42,7 +42,7 @@ pub fn convert_touch_input(
winit::event::TouchPhase::Ended => TouchPhase::Ended, winit::event::TouchPhase::Ended => TouchPhase::Ended,
winit::event::TouchPhase::Cancelled => TouchPhase::Cancelled, 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 { force: touch_input.force.map(|f| match f {
winit::event::Force::Calibrated { winit::event::Force::Calibrated {
force, force,

View file

@ -1,266 +1,112 @@
mod converters; mod converters;
mod system;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
mod web_resize; mod web_resize;
mod winit_config; mod winit_config;
mod winit_windows; 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_config::*;
pub use winit_windows::*; pub use winit_windows::*;
use bevy_app::{App, AppExit, CoreStage, Plugin}; use bevy_app::{App, AppExit, CoreStage, Plugin};
use bevy_ecs::event::{Events, ManualEventReader};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_ecs::{ use bevy_input::{
event::{Events, ManualEventReader}, keyboard::KeyboardInput,
world::World, mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel},
touch::TouchInput,
}; };
use bevy_input::mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}; use bevy_math::{ivec2, DVec2, Vec2};
use bevy_math::{ivec2, DVec2, UVec2, Vec2};
use bevy_utils::{ use bevy_utils::{
tracing::{error, info, trace, warn}, tracing::{trace, warn},
Instant, Instant,
}; };
use bevy_window::{ use bevy_window::{
CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows, ReceivedCharacter,
ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated,
WindowClosed, WindowCreated, WindowFocused, WindowMoved, WindowResized, WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged,
WindowScaleFactorChanged, Windows,
}; };
use winit::{ use winit::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition},
event::{self, DeviceEvent, Event, StartCause, WindowEvent}, event::{self, DeviceEvent, Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
}; };
use crate::system::WinitWindowInfo;
#[cfg(target_arch = "wasm32")]
use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin};
#[derive(Default)] #[derive(Default)]
pub struct WinitPlugin; pub struct WinitPlugin;
impl Plugin for WinitPlugin { impl Plugin for WinitPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
let event_loop = EventLoop::new();
app.insert_non_send_resource(event_loop);
app.init_non_send_resource::<WinitWindows>() app.init_non_send_resource::<WinitWindows>()
.init_resource::<WinitSettings>() .init_resource::<WinitSettings>()
.set_runner(winit_runner) .set_runner(winit_runner)
.add_system_to_stage(CoreStage::PostUpdate, change_window.label(ModifiesWindows)); .add_system_set_to_stage(
CoreStage::PostUpdate,
SystemSet::new()
.label(ModifiesWindows)
.with_system(changed_window)
.with_system(despawn_window),
);
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
app.add_plugin(web_resize::CanvasParentResizePlugin); app.add_plugin(CanvasParentResizePlugin);
let event_loop = EventLoop::new();
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] #[cfg(not(target_arch = "wasm32"))]
let mut create_window_reader = WinitCreateWindowReader::default(); let mut create_window_system_state: SystemState<(
#[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] Commands,
let create_window_reader = WinitCreateWindowReader::default(); NonSendMut<EventLoop<()>>,
// Note that we create a window here "early" because WASM/WebGL requires the window to exist prior to initializing Query<(Entity, &mut Window)>,
// the renderer. EventWriter<WindowCreated>,
NonSendMut<WinitWindows>,
)> = SystemState::from_world(&mut app.world);
#[cfg(target_arch = "wasm32")]
let mut create_window_system_state: SystemState<(
Commands,
NonSendMut<EventLoop<()>>,
Query<(Entity, &mut Window)>,
EventWriter<WindowCreated>,
NonSendMut<WinitWindows>,
ResMut<CanvasParentResizeEventChannel>,
)> = SystemState::from_world(&mut app.world);
// And for ios and macos, we should not create window early, all ui related code should be executed inside // And for ios and macos, we should not create window early, all ui related code should be executed inside
// UIApplicationMain/NSApplicationMain. // UIApplicationMain/NSApplicationMain.
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] #[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) #[cfg(not(target_arch = "wasm32"))]
.insert_non_send_resource(event_loop); let (commands, event_loop, mut new_windows, event_writer, winit_windows) =
} create_window_system_state.get_mut(&mut app.world);
}
fn change_window( #[cfg(target_arch = "wasm32")]
mut winit_windows: NonSendMut<WinitWindows>, let (commands, event_loop, mut new_windows, event_writer, winit_windows, event_channel) =
mut windows: ResMut<Windows>, create_window_system_state.get_mut(&mut app.world);
mut window_dpi_changed_events: EventWriter<WindowScaleFactorChanged>,
mut window_close_events: EventWriter<WindowClosed>,
) {
let mut removed_windows = vec![];
for bevy_window in windows.iter_mut() {
let id = bevy_window.id();
for command in bevy_window.drain_commands() {
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::<f64>(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::<f32>(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::*; // Here we need to create a winit-window and give it a WindowHandle which the renderer can use.
let maybe_monitor = match monitor_selection { // It needs to be spawned before the start of the startup-stage, so we cannot use a regular system.
Current => window.current_monitor(), // Instead we need to create the window and spawn it using direct world access
Primary => window.primary_monitor(), create_window(
Index(i) => window.available_monitors().nth(i), commands,
}; &event_loop,
if let Some(monitor) = maybe_monitor { new_windows.iter_mut(),
let monitor_position = DVec2::from(<(_, _)>::from(monitor.position())); event_writer,
let position = monitor_position + position.as_dvec2(); winit_windows,
#[cfg(target_arch = "wasm32")]
window.set_outer_position(LogicalPosition::new(position.x, position.y)); event_channel,
} 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::<f64>();
let window_size = window.outer_size();
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,
};
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 });
} }
create_window_system_state.apply(&mut app.world);
} }
} }
@ -307,8 +153,30 @@ where
panic!("Run return is not supported on this platform!") panic!("Run return is not supported on this platform!")
} }
pub fn winit_runner(app: App) { #[derive(SystemParam)]
winit_runner_with(app); 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( // #[cfg(any(
@ -347,19 +215,13 @@ impl Default for WinitPersistentState {
} }
} }
#[derive(Default, Resource)] pub fn winit_runner(mut app: App) {
struct WinitCreateWindowReader(ManualEventReader<CreateWindow>); // We remove this so that we have ownership over it.
pub fn winit_runner_with(mut app: App) {
let mut event_loop = app let mut event_loop = app
.world .world
.remove_non_send_resource::<EventLoop<()>>() .remove_non_send_resource::<EventLoop<()>>()
.unwrap(); .unwrap();
let mut create_window_event_reader = app
.world
.remove_resource::<WinitCreateWindowReader>()
.unwrap()
.0;
let mut app_exit_event_reader = ManualEventReader::<AppExit>::default(); let mut app_exit_event_reader = ManualEventReader::<AppExit>::default();
let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default(); let mut redraw_event_reader = ManualEventReader::<RequestRedraw>::default();
let mut winit_state = WinitPersistentState::default(); let mut winit_state = WinitPersistentState::default();
@ -370,23 +232,80 @@ pub fn winit_runner_with(mut app: App) {
trace!("Entering winit event loop"); trace!("Entering winit event loop");
let mut focused_window_state: SystemState<(Res<WinitSettings>, 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<Window>>,
EventWriter<WindowCreated>,
NonSendMut<WinitWindows>,
)> = SystemState::from_world(&mut app.world);
#[cfg(target_arch = "wasm32")]
let mut create_window_system_state: SystemState<(
Commands,
Query<(Entity, &mut Window), Added<Window>>,
EventWriter<WindowCreated>,
NonSendMut<WinitWindows>,
ResMut<CanvasParentResizeEventChannel>,
)> = SystemState::from_world(&mut app.world);
let event_handler = move |event: Event<()>, let event_handler = move |event: Event<()>,
event_loop: &EventLoopWindowTarget<()>, event_loop: &EventLoopWindowTarget<()>,
control_flow: &mut ControlFlow| { control_flow: &mut ControlFlow| {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); let _span = bevy_utils::tracing::info_span!("winit event_handler").entered();
if let Some(app_exit_events) = app.world.get_resource::<Events<AppExit>>() {
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 { match event {
event::Event::NewEvents(start) => { event::Event::NewEvents(start) => {
let winit_config = app.world.resource::<WinitSettings>(); let (winit_config, window_focused_query) = focused_window_state.get(&app.world);
let windows = app.world.resource::<Windows>();
let focused = windows.iter().any(|w| w.is_focused()); let app_focused = window_focused_query.iter().any(|window| window.focused);
// Check if either the `WaitUntil` timeout was triggered by winit, or that same // 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 // 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 // because we don't know if the criteria for an app update were met until the end of
// the frame. // the frame.
let auto_timeout_reached = matches!(start, StartCause::ResumeTimeReached { .. }); let auto_timeout_reached = matches!(start, StartCause::ResumeTimeReached { .. });
let now = Instant::now(); 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::Continuous => false,
UpdateMode::Reactive { max_wait } UpdateMode::Reactive { max_wait }
| UpdateMode::ReactiveLowPower { max_wait } => { | UpdateMode::ReactiveLowPower { max_wait } => {
@ -402,81 +321,120 @@ pub fn winit_runner_with(mut app: App) {
window_id: winit_window_id, window_id: winit_window_id,
.. ..
} => { } => {
let world = app.world.cell(); // Fetch and prepare details from the world
let winit_windows = world.non_send_resource_mut::<WinitWindows>(); let mut system_state: SystemState<(
let mut windows = world.resource_mut::<Windows>(); NonSend<WinitWindows>,
let window_id = Query<(&mut Window, &mut WinitWindowInfo)>,
if let Some(window_id) = winit_windows.get_window_id(winit_window_id) { WindowEvents,
window_id InputEvents,
CursorEvents,
EventWriter<FileDragAndDrop>,
)> = 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 { } else {
warn!( warn!(
"Skipped event for unknown winit Window Id {:?}", "Skipped event {:?} for unknown winit Window Id {:?}",
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; 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; winit_state.low_power_event = true;
match event { match event {
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
window.update_actual_size_from_backend(size.width, size.height); window
world.send_event(WindowResized { .resolution
id: window_id, .set_physical_resolution(size.width, size.height);
info.last_winit_size = size;
window_events.window_resized.send(WindowResized {
window: window_entity,
width: window.width(), width: window.width(),
height: window.height(), height: window.height(),
}); });
} }
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
world.send_event(WindowCloseRequested { id: window_id }); window_events
.window_close_requested
.send(WindowCloseRequested {
window: window_entity,
});
} }
WindowEvent::KeyboardInput { ref input, .. } => { 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, .. } => { WindowEvent::CursorMoved { position, .. } => {
let winit_window = winit_windows.get_window(window_id).unwrap(); let physical_position = DVec2::new(
let inner_size = winit_window.inner_size(); position.x,
// Flip the coordinate space from winit's context to our context.
window.resolution.physical_height() as f64 - position.y,
);
// move origin to bottom left window.cursor.position = Some(physical_position);
let y_position = inner_size.height as f64 - position.y;
let physical_position = DVec2::new(position.x, y_position); cursor_events.cursor_moved.send(CursorMoved {
window window: window_entity,
.update_cursor_physical_position_from_backend(Some(physical_position)); position: (physical_position / window.resolution.scale_factor())
.as_vec2(),
world.send_event(CursorMoved {
id: window_id,
position: (physical_position / window.scale_factor()).as_vec2(),
}); });
} }
WindowEvent::CursorEntered { .. } => { WindowEvent::CursorEntered { .. } => {
world.send_event(CursorEntered { id: window_id }); cursor_events.cursor_entered.send(CursorEntered {
window: window_entity,
});
} }
WindowEvent::CursorLeft { .. } => { WindowEvent::CursorLeft { .. } => {
window.update_cursor_physical_position_from_backend(None); // Component
world.send_event(CursorLeft { id: window_id }); 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, .. } => { WindowEvent::MouseInput { state, button, .. } => {
world.send_event(MouseButtonInput { input_events.mouse_button_input.send(MouseButtonInput {
button: converters::convert_mouse_button(button), button: converters::convert_mouse_button(button),
state: converters::convert_element_state(state), state: converters::convert_element_state(state),
}); });
} }
WindowEvent::MouseWheel { delta, .. } => match delta { WindowEvent::MouseWheel { delta, .. } => match delta {
event::MouseScrollDelta::LineDelta(x, y) => { event::MouseScrollDelta::LineDelta(x, y) => {
world.send_event(MouseWheel { input_events.mouse_wheel_input.send(MouseWheel {
unit: MouseScrollUnit::Line, unit: MouseScrollUnit::Line,
x, x,
y, y,
}); });
} }
event::MouseScrollDelta::PixelDelta(p) => { event::MouseScrollDelta::PixelDelta(p) => {
world.send_event(MouseWheel { input_events.mouse_wheel_input.send(MouseWheel {
unit: MouseScrollUnit::Pixel, unit: MouseScrollUnit::Pixel,
x: p.x as f32, x: p.x as f32,
y: p.y as f32, y: p.y as f32,
@ -484,12 +442,23 @@ pub fn winit_runner_with(mut app: App) {
} }
}, },
WindowEvent::Touch(touch) => { WindowEvent::Touch(touch) => {
let location = touch.location.to_logical(window.scale_factor()); let mut location =
world.send_event(converters::convert_touch_input(touch, 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) => { WindowEvent::ReceivedCharacter(c) => {
world.send_event(ReceivedCharacter { input_events.character_input.send(ReceivedCharacter {
id: window_id, window: window_entity,
char: c, char: c,
}); });
} }
@ -497,73 +466,84 @@ pub fn winit_runner_with(mut app: App) {
scale_factor, scale_factor,
new_inner_size, new_inner_size,
} => { } => {
world.send_event(WindowBackendScaleFactorChanged { window_events.window_backend_scale_factor_changed.send(
id: window_id, WindowBackendScaleFactorChanged {
scale_factor, window: window_entity,
}); 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() { 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 // If there is a scale factor override, then force that to be used
// Otherwise, use the OS suggested size // Otherwise, use the OS suggested size
// We have already told the OS about our resize constraints, so // We have already told the OS about our resize constraints, so
// the new_inner_size should take those into account // the new_inner_size should take those into account
*new_inner_size = winit::dpi::LogicalSize::new( *new_inner_size =
window.requested_width(), winit::dpi::LogicalSize::new(window.width(), window.height())
window.requested_height(), .to_physical::<u32>(forced_factor);
) // TODO: Should this not trigger a WindowsScaleFactorChanged?
.to_physical::<u32>(forced_factor);
} else if approx::relative_ne!(new_factor, prior_factor) { } else if approx::relative_ne!(new_factor, prior_factor) {
world.send_event(WindowScaleFactorChanged { // Trigger a change event if they are approximately different
id: window_id, window_events.window_scale_factor_changed.send(
scale_factor, WindowScaleFactorChanged {
}); window: window_entity,
scale_factor,
},
);
} }
let new_logical_width = new_inner_size.width as f64 / new_factor; 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; let new_logical_height = (new_inner_size.height as f64 / new_factor) as f32;
if approx::relative_ne!(window.width() as f64, new_logical_width) if approx::relative_ne!(window.width(), new_logical_width)
|| approx::relative_ne!(window.height() as f64, new_logical_height) || approx::relative_ne!(window.height(), new_logical_height)
{ {
world.send_event(WindowResized { window_events.window_resized.send(WindowResized {
id: window_id, window: window_entity,
width: new_logical_width as f32, width: new_logical_width,
height: new_logical_height as f32, height: new_logical_height,
}); });
} }
window.update_actual_size_from_backend( window
new_inner_size.width, .resolution
new_inner_size.height, .set_physical_resolution(new_inner_size.width, new_inner_size.height);
);
} }
WindowEvent::Focused(focused) => { WindowEvent::Focused(focused) => {
window.update_focused_status_from_backend(focused); // Component
world.send_event(WindowFocused { window.focused = focused;
id: window_id,
window_events.window_focused.send(WindowFocused {
window: window_entity,
focused, focused,
}); });
} }
WindowEvent::DroppedFile(path_buf) => { WindowEvent::DroppedFile(path_buf) => {
world.send_event(FileDragAndDrop::DroppedFile { file_drag_and_drop_events.send(FileDragAndDrop::DroppedFile {
id: window_id, window: window_entity,
path_buf, path_buf,
}); });
} }
WindowEvent::HoveredFile(path_buf) => { WindowEvent::HoveredFile(path_buf) => {
world.send_event(FileDragAndDrop::HoveredFile { file_drag_and_drop_events.send(FileDragAndDrop::HoveredFile {
id: window_id, window: window_entity,
path_buf, path_buf,
}); });
} }
WindowEvent::HoveredFileCancelled => { WindowEvent::HoveredFileCancelled => {
world.send_event(FileDragAndDrop::HoveredFileCancelled { id: window_id }); file_drag_and_drop_events.send(FileDragAndDrop::HoveredFileCancelled {
window: window_entity,
});
} }
WindowEvent::Moved(position) => { WindowEvent::Moved(position) => {
let position = ivec2(position.x, position.y); let position = ivec2(position.x, position.y);
window.update_actual_position_from_backend(position);
world.send_event(WindowMoved { window.position.set(position);
id: window_id,
window_events.window_moved.send(WindowMoved {
entity: window_entity,
position, position,
}); });
} }
@ -574,8 +554,12 @@ pub fn winit_runner_with(mut app: App) {
event: DeviceEvent::MouseMotion { delta: (x, y) }, event: DeviceEvent::MouseMotion { delta: (x, y) },
.. ..
} => { } => {
app.world.send_event(MouseMotion { let mut system_state: SystemState<EventWriter<MouseMotion>> =
delta: DVec2 { x, y }.as_vec2(), 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 => { event::Event::Suspended => {
@ -585,16 +569,12 @@ pub fn winit_runner_with(mut app: App) {
winit_state.active = true; winit_state.active = true;
} }
event::Event::MainEventsCleared => { event::Event::MainEventsCleared => {
handle_create_window_events( let (winit_config, window_focused_query) = focused_window_state.get(&app.world);
&mut app.world,
event_loop,
&mut create_window_event_reader,
);
let winit_config = app.world.resource::<WinitSettings>();
let update = if winit_state.active { let update = if winit_state.active {
let windows = app.world.resource::<Windows>(); // True if _any_ windows are currently being focused
let focused = windows.iter().any(|w| w.is_focused()); let app_focused = window_focused_query.iter().any(|window| window.focused);
match winit_config.update_mode(focused) { match winit_config.update_mode(app_focused) {
UpdateMode::Continuous | UpdateMode::Reactive { .. } => true, UpdateMode::Continuous | UpdateMode::Reactive { .. } => true,
UpdateMode::ReactiveLowPower { .. } => { UpdateMode::ReactiveLowPower { .. } => {
winit_state.low_power_event winit_state.low_power_event
@ -605,6 +585,7 @@ pub fn winit_runner_with(mut app: App) {
} else { } else {
false false
}; };
if update { if update {
winit_state.last_update = Instant::now(); winit_state.last_update = Instant::now();
app.update(); app.update();
@ -612,12 +593,15 @@ pub fn winit_runner_with(mut app: App) {
} }
Event::RedrawEventsCleared => { Event::RedrawEventsCleared => {
{ {
let winit_config = app.world.resource::<WinitSettings>(); // Fetch from world
let windows = app.world.resource::<Windows>(); let (winit_config, window_focused_query) = focused_window_state.get(&app.world);
let focused = windows.iter().any(|w| w.is_focused());
// True if _any_ windows are currently being focused
let app_focused = window_focused_query.iter().any(|window| window.focused);
let now = Instant::now(); let now = Instant::now();
use UpdateMode::*; use UpdateMode::*;
*control_flow = match winit_config.update_mode(focused) { *control_flow = match winit_config.update_mode(app_focused) {
Continuous => ControlFlow::Poll, Continuous => ControlFlow::Poll,
Reactive { max_wait } | ReactiveLowPower { max_wait } => { Reactive { max_wait } | ReactiveLowPower { max_wait } => {
if let Some(instant) = now.checked_add(*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, // 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 // we won't be able to see redraw requests until the next event, defeating the
// purpose of a redraw request! // purpose of a redraw request!
@ -638,64 +623,18 @@ pub fn winit_runner_with(mut app: App) {
redraw = true; redraw = true;
} }
} }
if let Some(app_exit_events) = app.world.get_resource::<Events<AppExit>>() {
if app_exit_event_reader.iter(app_exit_events).last().is_some() {
*control_flow = ControlFlow::Exit;
}
}
winit_state.redraw_request_sent = redraw; winit_state.redraw_request_sent = redraw;
} }
_ => (), _ => (),
} }
}; };
// If true, returns control from Winit back to the main Bevy loop
if return_from_run { if return_from_run {
run_return(&mut event_loop, event_handler); run_return(&mut event_loop, event_handler);
} else { } else {
run(event_loop, event_handler); run(event_loop, event_handler);
} }
} }
fn handle_create_window_events(
world: &mut World,
event_loop: &EventLoopWindowTarget<()>,
create_window_event_reader: &mut ManualEventReader<CreateWindow>,
) {
let world = world.cell();
let mut winit_windows = world.non_send_resource_mut::<WinitWindows>();
let mut windows = world.resource_mut::<Windows>();
let create_window_events = world.resource::<Events<CreateWindow>>();
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::<web_resize::CanvasParentResizeEventChannel>();
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);
}
}
}
}

View file

@ -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<Item = (Entity, Mut<'a, Window>)>,
mut event_writer: EventWriter<WindowCreated>,
mut winit_windows: NonSendMut<WinitWindows>,
#[cfg(target_arch = "wasm32")] event_channel: ResMut<CanvasParentResizeEventChannel>,
) {
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<Entity, String>);
pub(crate) fn despawn_window(
closed: RemovedComponents<Window>,
mut close_events: EventWriter<WindowClosed>,
mut winit_windows: NonSendMut<WinitWindows>,
) {
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<u32>,
}
// 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<Window>>,
winit_windows: NonSendMut<WinitWindows>,
) {
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();
}
}
}

View file

@ -1,7 +1,6 @@
use crate::WinitWindows; use crate::WinitWindows;
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_window::WindowId;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
@ -17,7 +16,7 @@ impl Plugin for CanvasParentResizePlugin {
struct ResizeEvent { struct ResizeEvent {
size: LogicalSize<f32>, size: LogicalSize<f32>,
window_id: WindowId, window: Entity,
} }
#[derive(Resource)] #[derive(Resource)]
@ -31,7 +30,7 @@ fn canvas_parent_resize_event_handler(
resize_events: Res<CanvasParentResizeEventChannel>, resize_events: Res<CanvasParentResizeEventChannel>,
) { ) {
for event in resize_events.receiver.try_iter() { 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); window.set_inner_size(event.size);
} }
} }
@ -59,12 +58,12 @@ impl Default for CanvasParentResizeEventChannel {
} }
impl 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 sender = self.sender.clone();
let owned_selector = selector.to_string(); let owned_selector = selector.to_string();
let resize = move || { let resize = move || {
if let Some(size) = get_size(&owned_selector) { if let Some(size) = get_size(&owned_selector) {
sender.send(ResizeEvent { size, window_id }).unwrap(); sender.send(ResizeEvent { size, window }).unwrap();
} }
}; };

View file

@ -1,20 +1,18 @@
use bevy_math::{DVec2, IVec2}; use bevy_ecs::entity::Entity;
use bevy_utils::HashMap;
use bevy_window::{ use bevy_utils::{tracing::warn, HashMap};
CursorGrabMode, MonitorSelection, RawHandleWrapper, Window, WindowDescriptor, WindowId, use bevy_window::{CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution};
WindowMode,
};
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use winit::{ use winit::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, dpi::{LogicalSize, PhysicalPosition},
window::Fullscreen, monitor::MonitorHandle,
}; };
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct WinitWindows { pub struct WinitWindows {
pub windows: HashMap<winit::window::WindowId, winit::window::Window>, pub windows: HashMap<winit::window::WindowId, winit::window::Window>,
pub window_id_to_winit: HashMap<WindowId, winit::window::WindowId>, pub entity_to_winit: HashMap<Entity, winit::window::WindowId>,
pub winit_to_window_id: HashMap<winit::window::WindowId, WindowId>, pub winit_to_entity: HashMap<winit::window::WindowId, Entity>,
// Some winit functions, such as `set_window_icon` can only be used from the main thread. If // 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 // 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. // only ever accessed with bevy's non-send functions and in NonSend systems.
@ -25,45 +23,40 @@ impl WinitWindows {
pub fn create_window( pub fn create_window(
&mut self, &mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<()>, event_loop: &winit::event_loop::EventLoopWindowTarget<()>,
window_id: WindowId, entity: Entity,
window_descriptor: &WindowDescriptor, window: &Window,
) -> Window { ) -> &winit::window::Window {
let mut winit_window_builder = winit::window::WindowBuilder::new(); let mut winit_window_builder = winit::window::WindowBuilder::new();
let &WindowDescriptor { winit_window_builder = match window.mode {
width, WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some(
height, winit::window::Fullscreen::Borderless(event_loop.primary_monitor()),
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())),
)), )),
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( WindowMode::SizedFullscreen => winit_window_builder.with_fullscreen(Some(
Fullscreen::Exclusive(get_fitting_videomode( winit::window::Fullscreen::Exclusive(get_fitting_videomode(
&selected_or_primary_monitor.unwrap(), &event_loop.primary_monitor().unwrap(),
window_descriptor.width as u32, window.width() as u32,
window_descriptor.height as u32, window.height() as u32,
)), )),
)), )),
WindowMode::Windowed => { 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::<f64>(sf)) winit_window_builder.with_inner_size(logical_size.to_physical::<f64>(sf))
} else { } else {
winit_window_builder.with_inner_size(logical_size) winit_window_builder.with_inner_size(logical_size)
@ -72,12 +65,12 @@ impl WinitWindows {
}; };
winit_window_builder = winit_window_builder winit_window_builder = winit_window_builder
.with_resizable(window_descriptor.resizable) .with_always_on_top(window.always_on_top)
.with_decorations(window_descriptor.decorations) .with_resizable(window.resizable)
.with_transparent(window_descriptor.transparent) .with_decorations(window.decorations)
.with_always_on_top(window_descriptor.always_on_top); .with_transparent(window.transparent);
let constraints = window_descriptor.resize_constraints.check_constraints(); let constraints = window.resize_constraints.check_constraints();
let min_inner_size = LogicalSize { let min_inner_size = LogicalSize {
width: constraints.min_width, width: constraints.min_width,
height: constraints.min_height, height: constraints.min_height,
@ -97,14 +90,14 @@ impl WinitWindows {
}; };
#[allow(unused_mut)] #[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")] #[cfg(target_arch = "wasm32")]
{ {
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use winit::platform::web::WindowBuilderExtWebSys; 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 window = web_sys::window().unwrap();
let document = window.document().unwrap(); let document = window.document().unwrap();
let canvas = document let canvas = document
@ -121,59 +114,21 @@ impl WinitWindows {
let winit_window = winit_window_builder.build(event_loop).unwrap(); let winit_window = winit_window_builder.build(event_loop).unwrap();
if window_descriptor.mode == WindowMode::Windowed { // Do not set the grab mode on window creation if it's none, this can fail on mobile
use bevy_window::WindowPosition::*; if window.cursor.grab_mode != CursorGrabMode::None {
match position { attempt_grab(&winit_window, window.cursor.grab_mode);
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::<f64>();
let size = monitor.size();
// Logical to physical window size
let PhysicalSize::<u32> { 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::<f64>(sf),
);
} else {
winit_window
.set_outer_position(LogicalPosition::new(position.x, position.y));
}
}
}
}
} }
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.entity_to_winit.insert(entity, winit_window.id());
self.winit_to_window_id.insert(winit_window.id(), window_id); self.winit_to_entity.insert(winit_window.id(), entity);
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
use winit::platform::web::WindowExtWebSys; use winit::platform::web::WindowExtWebSys;
if window_descriptor.canvas.is_none() { if window.canvas.is_none() {
let canvas = winit_window.canvas(); let canvas = winit_window.canvas();
let window = web_sys::window().unwrap(); let window = web_sys::window().unwrap();
@ -185,45 +140,31 @@ impl WinitWindows {
} }
} }
let position = winit_window self.windows
.outer_position() .entry(winit_window.id())
.ok() .insert(winit_window)
.map(|position| IVec2::new(position.x, position.y)); .into_mut()
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
} }
pub fn get_window(&self, id: WindowId) -> Option<&winit::window::Window> { /// Get the winit window that is associated with our entity.
self.window_id_to_winit pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> {
.get(&id) self.entity_to_winit
.and_then(|id| self.windows.get(id)) .get(&entity)
.and_then(|winit_id| self.windows.get(winit_id))
} }
pub fn get_window_id(&self, id: winit::window::WindowId) -> Option<WindowId> { /// Get the entity associated with the winit window id.
self.winit_to_window_id.get(&id).cloned() ///
/// This is mostly just an intermediary step between us and winit.
pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option<Entity> {
self.winit_to_entity.get(&winit_id).cloned()
} }
pub fn remove_window(&mut self, id: WindowId) -> Option<winit::window::Window> { /// Remove a window from winit.
let winit_id = self.window_id_to_winit.remove(&id)?; ///
/// This should mostly just be called when the window is closing.
pub fn remove_window(&mut self, entity: Entity) -> Option<winit::window::Window> {
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 // Don't remove from winit_to_window_id, to track that we used to know about this winit window
self.windows.remove(&winit_id) self.windows.remove(&winit_id)
} }
@ -278,3 +219,89 @@ pub fn get_best_videomode(monitor: &winit::monitor::MonitorHandle) -> winit::mon
modes.first().unwrap().clone() 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<Item = MonitorHandle>,
primary_monitor: Option<MonitorHandle>,
current_monitor: Option<MonitorHandle>,
) -> Option<PhysicalPosition<i32>> {
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::<u32>(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::<i32>())
} 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::<i32>())
}
}
}
// 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 {}

View file

@ -3,10 +3,8 @@
use std::f32::consts::PI; use std::f32::consts::PI;
use bevy::{ use bevy::{
core_pipeline::clear_color::ClearColorConfig, core_pipeline::clear_color::ClearColorConfig, prelude::*, render::camera::Viewport,
prelude::*, window::WindowResized,
render::camera::Viewport,
window::{WindowId, WindowResized},
}; };
fn main() { fn main() {
@ -82,7 +80,7 @@ struct LeftCamera;
struct RightCamera; struct RightCamera;
fn set_camera_viewports( fn set_camera_viewports(
windows: Res<Windows>, windows: Query<&Window>,
mut resize_events: EventReader<WindowResized>, mut resize_events: EventReader<WindowResized>,
mut left_camera: Query<&mut Camera, (With<LeftCamera>, Without<RightCamera>)>, mut left_camera: Query<&mut Camera, (With<LeftCamera>, Without<RightCamera>)>,
mut right_camera: Query<&mut Camera, With<RightCamera>>, mut right_camera: Query<&mut Camera, With<RightCamera>>,
@ -91,21 +89,25 @@ fn set_camera_viewports(
// so then each camera always takes up half the screen. // 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. // 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() { for resize_event in resize_events.iter() {
if resize_event.id == WindowId::primary() { let window = windows.get(resize_event.window).unwrap();
let window = windows.primary(); let mut left_camera = left_camera.single_mut();
let mut left_camera = left_camera.single_mut(); left_camera.viewport = Some(Viewport {
left_camera.viewport = Some(Viewport { physical_position: UVec2::new(0, 0),
physical_position: UVec2::new(0, 0), physical_size: UVec2::new(
physical_size: UVec2::new(window.physical_width() / 2, window.physical_height()), window.resolution.physical_width() / 2,
..default() window.resolution.physical_height(),
}); ),
..default()
});
let mut right_camera = right_camera.single_mut(); let mut right_camera = right_camera.single_mut();
right_camera.viewport = Some(Viewport { right_camera.viewport = Some(Viewport {
physical_position: UVec2::new(window.physical_width() / 2, 0), physical_position: UVec2::new(window.resolution.physical_width() / 2, 0),
physical_size: UVec2::new(window.physical_width() / 2, window.physical_height()), physical_size: UVec2::new(
..default() window.resolution.physical_width() / 2,
}); window.resolution.physical_height(),
} ),
..default()
});
} }
} }

View file

@ -11,7 +11,7 @@
//! - <https://docs.rs/bevy/latest/bevy/app/struct.App.html#method.run> //! - <https://docs.rs/bevy/latest/bevy/app/struct.App.html#method.run>
//! - <https://docs.rs/bevy/latest/bevy/winit/struct.WinitSettings.html#structfield.return_from_run> //! - <https://docs.rs/bevy/latest/bevy/winit/struct.WinitSettings.html#structfield.return_from_run>
use bevy::{prelude::*, winit::WinitSettings}; use bevy::{prelude::*, window::WindowPlugin, winit::WinitSettings};
fn main() { fn main() {
println!("Running Bevy App"); println!("Running Bevy App");
@ -21,10 +21,10 @@ fn main() {
..default() ..default()
}) })
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
title: "Close the window to return to the main function".to_owned(), title: "Close the window to return to the main function".into(),
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_system(system) .add_system(system)

View file

@ -37,8 +37,8 @@ fn move_system(mut sprites: Query<(&mut Transform, &Velocity)>) {
} }
// Bounce sprites outside the window // Bounce sprites outside the window
fn bounce_system(windows: Res<Windows>, mut sprites: Query<(&Transform, &mut Velocity)>) { fn bounce_system(windows: Query<&Window>, mut sprites: Query<(&Transform, &mut Velocity)>) {
let window = windows.primary(); let window = windows.single();
let width = window.width(); let width = window.width();
let height = window.height(); let height = window.height();
let left = width / -2.0; let left = width / -2.0;

View file

@ -245,17 +245,15 @@ fn velocity_system(time: Res<Time>, mut velocity_query: Query<&mut Velocity>) {
/// velocity. On collision with the ground it applies an upwards /// velocity. On collision with the ground it applies an upwards
/// force. /// force.
fn collision_system( fn collision_system(
windows: Res<Windows>, windows: Query<&Window>,
mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>, mut query: Query<(&mut Velocity, &mut Transform), With<Contributor>>,
) { ) {
let Some(window) = windows.get_primary() else { let window = windows.single();
return;
};
let ceiling = window.height() / 2.; let ceiling = window.height() / 2.;
let ground = -(window.height() / 2.); let ground = -window.height() / 2.;
let wall_left = -(window.width() / 2.); let wall_left = -window.width() / 2.;
let wall_right = window.width() / 2.; let wall_right = window.width() / 2.;
// The maximum height the birbs should try to reach is one birb below the top of the window. // The maximum height the birbs should try to reach is one birb below the top of the window.

View file

@ -1,7 +1,6 @@
//! Demonstrates how to grab and hide the mouse cursor. //! Demonstrates how to grab and hide the mouse cursor.
use bevy::prelude::*; use bevy::{prelude::*, window::CursorGrabMode};
use bevy::window::CursorGrabMode;
fn main() { fn main() {
App::new() App::new()
@ -13,17 +12,19 @@ fn main() {
// This system grabs the mouse when the left mouse button is pressed // This system grabs the mouse when the left mouse button is pressed
// and releases it when the escape key is pressed // and releases it when the escape key is pressed
fn grab_mouse( fn grab_mouse(
mut windows: ResMut<Windows>, mut windows: Query<&mut Window>,
mouse: Res<Input<MouseButton>>, mouse: Res<Input<MouseButton>>,
key: Res<Input<KeyCode>>, key: Res<Input<KeyCode>>,
) { ) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
if mouse.just_pressed(MouseButton::Left) { if mouse.just_pressed(MouseButton::Left) {
window.set_cursor_visibility(false); window.cursor.visible = false;
window.set_cursor_grab_mode(CursorGrabMode::Locked); window.cursor.grab_mode = CursorGrabMode::Locked;
} }
if key.just_pressed(KeyCode::Escape) { if key.just_pressed(KeyCode::Escape) {
window.set_cursor_visibility(true); window.cursor.visible = true;
window.set_cursor_grab_mode(CursorGrabMode::None); window.cursor.grab_mode = CursorGrabMode::None;
} }
} }

View file

@ -5,11 +5,11 @@ use bevy::{input::touch::TouchPhase, prelude::*, window::WindowMode};
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
resizable: false, resizable: false,
mode: WindowMode::BorderlessFullscreen, mode: WindowMode::BorderlessFullscreen,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_startup_system(setup_scene) .add_startup_system(setup_scene)
@ -20,17 +20,18 @@ fn main() {
} }
fn touch_camera( fn touch_camera(
windows: ResMut<Windows>, windows: Query<&Window>,
mut touches: EventReader<TouchInput>, mut touches: EventReader<TouchInput>,
mut camera: Query<&mut Transform, With<Camera3d>>, mut camera: Query<&mut Transform, With<Camera3d>>,
mut last_position: Local<Option<Vec2>>, mut last_position: Local<Option<Vec2>>,
) { ) {
let window = windows.single();
for touch in touches.iter() { for touch in touches.iter() {
if touch.phase == TouchPhase::Started { if touch.phase == TouchPhase::Started {
*last_position = None; *last_position = None;
} }
if let Some(last_position) = *last_position { if let Some(last_position) = *last_position {
let window = windows.primary();
let mut transform = camera.single_mut(); let mut transform = camera.single_mut();
*transform = Transform::from_xyz( *transform = Transform::from_xyz(
transform.translation.x transform.translation.x

View file

@ -13,6 +13,7 @@ use bevy::{
renderer::{RenderContext, RenderDevice}, renderer::{RenderContext, RenderDevice},
RenderApp, RenderStage, RenderApp, RenderStage,
}, },
window::WindowPlugin,
}; };
use std::borrow::Cow; use std::borrow::Cow;
@ -23,11 +24,11 @@ fn main() {
App::new() App::new()
.insert_resource(ClearColor(Color::BLACK)) .insert_resource(ClearColor(Color::BLACK))
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
// uncomment for unthrottled FPS // uncomment for unthrottled FPS
// present_mode: bevy::window::PresentMode::AutoNoVsync, // present_mode: bevy::window::PresentMode::AutoNoVsync,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(GameOfLifeComputePlugin) .add_plugin(GameOfLifeComputePlugin)

View file

@ -34,16 +34,18 @@ struct MainCube;
fn setup( fn setup(
mut commands: Commands, mut commands: Commands,
mut windows: ResMut<Windows>, windows: Query<&Window>,
mut meshes: ResMut<Assets<Mesh>>, mut meshes: ResMut<Assets<Mesh>>,
mut post_processing_materials: ResMut<Assets<PostProcessingMaterial>>, mut post_processing_materials: ResMut<Assets<PostProcessingMaterial>>,
mut materials: ResMut<Assets<StandardMaterial>>, mut materials: ResMut<Assets<StandardMaterial>>,
mut images: ResMut<Assets<Image>>, mut images: ResMut<Assets<Image>>,
) { ) {
let window = windows.primary_mut(); // This assumes we only have a single window
let window = windows.single();
let size = Extent3d { let size = Extent3d {
width: window.physical_width(), width: window.resolution.physical_width(),
height: window.physical_height(), height: window.resolution.physical_height(),
..default() ..default()
}; };

View file

@ -6,7 +6,7 @@ use bevy::{
diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*, prelude::*,
time::FixedTimestep, time::FixedTimestep,
window::PresentMode, window::{PresentMode, WindowResolution},
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -30,14 +30,12 @@ struct Bird {
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
title: "BevyMark".to_string(), title: "BevyMark".into(),
width: 800., resolution: (800., 600.).into(),
height: 600.,
present_mode: PresentMode::AutoNoVsync, present_mode: PresentMode::AutoNoVsync,
resizable: true,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(FrameTimeDiagnosticsPlugin::default()) .add_plugin(FrameTimeDiagnosticsPlugin::default())
@ -67,15 +65,17 @@ struct BirdScheduled {
fn scheduled_spawner( fn scheduled_spawner(
mut commands: Commands, mut commands: Commands,
windows: Res<Windows>, windows: Query<&Window>,
mut scheduled: ResMut<BirdScheduled>, mut scheduled: ResMut<BirdScheduled>,
mut counter: ResMut<BevyCounter>, mut counter: ResMut<BevyCounter>,
bird_texture: Res<BirdTexture>, bird_texture: Res<BirdTexture>,
) { ) {
let window = windows.single();
if scheduled.wave > 0 { if scheduled.wave > 0 {
spawn_birds( spawn_birds(
&mut commands, &mut commands,
&windows, &window.resolution,
&mut counter, &mut counter,
scheduled.per_wave, scheduled.per_wave,
bird_texture.clone_weak(), bird_texture.clone_weak(),
@ -150,10 +150,12 @@ fn mouse_handler(
mut commands: Commands, mut commands: Commands,
time: Res<Time>, time: Res<Time>,
mouse_button_input: Res<Input<MouseButton>>, mouse_button_input: Res<Input<MouseButton>>,
windows: Res<Windows>, windows: Query<&Window>,
bird_texture: Res<BirdTexture>, bird_texture: Res<BirdTexture>,
mut counter: ResMut<BevyCounter>, mut counter: ResMut<BevyCounter>,
) { ) {
let window = windows.single();
if mouse_button_input.just_released(MouseButton::Left) { if mouse_button_input.just_released(MouseButton::Left) {
let mut rng = thread_rng(); let mut rng = thread_rng();
counter.color = Color::rgb_linear(rng.gen(), rng.gen(), rng.gen()); counter.color = Color::rgb_linear(rng.gen(), rng.gen(), rng.gen());
@ -163,7 +165,7 @@ fn mouse_handler(
let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_seconds_f64()) as usize; let spawn_count = (BIRDS_PER_SECOND as f64 * time.delta_seconds_f64()) as usize;
spawn_birds( spawn_birds(
&mut commands, &mut commands,
&windows, &window.resolution,
&mut counter, &mut counter,
spawn_count, spawn_count,
bird_texture.clone_weak(), bird_texture.clone_weak(),
@ -173,14 +175,13 @@ fn mouse_handler(
fn spawn_birds( fn spawn_birds(
commands: &mut Commands, commands: &mut Commands,
windows: &Windows, primary_window_resolution: &WindowResolution,
counter: &mut BevyCounter, counter: &mut BevyCounter,
spawn_count: usize, spawn_count: usize,
texture: Handle<Image>, texture: Handle<Image>,
) { ) {
let window = windows.primary(); let bird_x = (primary_window_resolution.width() / -2.) + HALF_BIRD_SIZE;
let bird_x = (window.width() / -2.) + HALF_BIRD_SIZE; let bird_y = (primary_window_resolution.height() / 2.) - HALF_BIRD_SIZE;
let bird_y = (window.height() / 2.) - HALF_BIRD_SIZE;
let mut rng = thread_rng(); let mut rng = thread_rng();
for count in 0..spawn_count { for count in 0..spawn_count {
@ -219,8 +220,9 @@ fn movement_system(time: Res<Time>, mut bird_query: Query<(&mut Bird, &mut Trans
} }
} }
fn collision_system(windows: Res<Windows>, mut bird_query: Query<(&mut Bird, &Transform)>) { fn collision_system(windows: Query<&Window>, mut bird_query: Query<(&mut Bird, &Transform)>) {
let window = windows.primary(); let window = windows.single();
let half_width = window.width() * 0.5; let half_width = window.width() * 0.5;
let half_height = window.height() * 0.5; let half_height = window.height() * 0.5;

View file

@ -1,7 +1,7 @@
use bevy::{ use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*, prelude::*,
window::PresentMode, window::{PresentMode, WindowPlugin},
}; };
// For a total of 110 * 110 = 12100 buttons with text // For a total of 110 * 110 = 12100 buttons with text
@ -12,10 +12,10 @@ const FONT_SIZE: f32 = 7.0;
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
present_mode: PresentMode::Immediate, present_mode: PresentMode::Immediate,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(FrameTimeDiagnosticsPlugin::default()) .add_plugin(FrameTimeDiagnosticsPlugin::default())

View file

@ -16,16 +16,16 @@ use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
math::{DVec2, DVec3}, math::{DVec2, DVec3},
prelude::*, prelude::*,
window::PresentMode, window::{PresentMode, WindowPlugin},
}; };
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(FrameTimeDiagnosticsPlugin::default()) .add_plugin(FrameTimeDiagnosticsPlugin::default())

View file

@ -7,7 +7,7 @@ use std::time::Duration;
use bevy::{ use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*, prelude::*,
window::PresentMode, window::{PresentMode, WindowPlugin},
}; };
#[derive(Resource)] #[derive(Resource)]
@ -20,11 +20,11 @@ struct Foxes {
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".to_string(), title: "🦊🦊🦊 Many Foxes! 🦊🦊🦊".into(),
present_mode: PresentMode::AutoNoVsync, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(FrameTimeDiagnosticsPlugin) .add_plugin(FrameTimeDiagnosticsPlugin)

View file

@ -9,20 +9,19 @@ use bevy::{
pbr::{ExtractedPointLight, GlobalLightMeta}, pbr::{ExtractedPointLight, GlobalLightMeta},
prelude::*, prelude::*,
render::{camera::ScalingMode, Extract, RenderApp, RenderStage}, render::{camera::ScalingMode, Extract, RenderApp, RenderStage},
window::PresentMode, window::{PresentMode, WindowPlugin},
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
width: 1024.0, resolution: (1024.0, 768.0).into(),
height: 768.0, title: "many_lights".into(),
title: "many_lights".to_string(),
present_mode: PresentMode::AutoNoVsync, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(FrameTimeDiagnosticsPlugin::default()) .add_plugin(FrameTimeDiagnosticsPlugin::default())

View file

@ -10,7 +10,7 @@
use bevy::{ use bevy::{
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*, prelude::*,
window::PresentMode, window::{PresentMode, WindowPlugin},
}; };
use rand::Rng; use rand::Rng;
@ -31,10 +31,10 @@ fn main() {
.add_plugin(LogDiagnosticsPlugin::default()) .add_plugin(LogDiagnosticsPlugin::default())
.add_plugin(FrameTimeDiagnosticsPlugin::default()) .add_plugin(FrameTimeDiagnosticsPlugin::default())
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_startup_system(setup) .add_startup_system(setup)

View file

@ -97,14 +97,13 @@ impl Plugin for CameraControllerPlugin {
fn camera_controller( fn camera_controller(
time: Res<Time>, time: Res<Time>,
mut windows: ResMut<Windows>, mut windows: Query<&mut Window>,
mut mouse_events: EventReader<MouseMotion>, mut mouse_events: EventReader<MouseMotion>,
mouse_button_input: Res<Input<MouseButton>>, mouse_button_input: Res<Input<MouseButton>>,
key_input: Res<Input<KeyCode>>, key_input: Res<Input<KeyCode>>,
mut move_toggled: Local<bool>, mut move_toggled: Local<bool>,
mut query: Query<(&mut Transform, &mut CameraController), With<Camera>>, mut query: Query<(&mut Transform, &mut CameraController), With<Camera>>,
) { ) {
let window = windows.primary_mut();
let dt = time.delta_seconds(); let dt = time.delta_seconds();
if let Ok((mut transform, mut options)) = query.get_single_mut() { if let Ok((mut transform, mut options)) = query.get_single_mut() {
@ -166,15 +165,23 @@ fn camera_controller(
// Handle mouse input // Handle mouse input
let mut mouse_delta = Vec2::ZERO; let mut mouse_delta = Vec2::ZERO;
if mouse_button_input.pressed(options.mouse_key_enable_mouse) || *move_toggled { if mouse_button_input.pressed(options.mouse_key_enable_mouse) || *move_toggled {
window.set_cursor_grab_mode(CursorGrabMode::Locked); for mut window in &mut windows {
window.set_cursor_visibility(false); if !window.focused {
continue;
}
window.cursor.grab_mode = CursorGrabMode::Locked;
window.cursor.visible = false;
}
for mouse_event in mouse_events.iter() { for mouse_event in mouse_events.iter() {
mouse_delta += mouse_event.delta; mouse_delta += mouse_event.delta;
} }
} else { } else {
window.set_cursor_grab_mode(CursorGrabMode::None); for mut window in &mut windows {
window.set_cursor_visibility(true); window.cursor.grab_mode = CursorGrabMode::None;
window.cursor.visible = true;
}
} }
if mouse_delta != Vec2::ZERO { if mouse_delta != Vec2::ZERO {

View file

@ -9,6 +9,7 @@ use bevy::{
math::Vec3A, math::Vec3A,
prelude::*, prelude::*,
render::primitives::{Aabb, Sphere}, render::primitives::{Aabb, Sphere},
window::WindowPlugin,
}; };
mod camera_controller_plugin; mod camera_controller_plugin;
@ -26,10 +27,10 @@ fn main() {
.add_plugins( .add_plugins(
DefaultPlugins DefaultPlugins
.set(WindowPlugin { .set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
title: "bevy scene viewer".to_string(), title: "bevy scene viewer".to_string(),
..default() ..default()
}, }),
..default() ..default()
}) })
.set(AssetPlugin { .set(AssetPlugin {

View file

@ -3,16 +3,16 @@
use bevy::{ use bevy::{
diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin}, diagnostic::{Diagnostics, FrameTimeDiagnosticsPlugin},
prelude::*, prelude::*,
window::PresentMode, window::{PresentMode, WindowPlugin},
}; };
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
present_mode: PresentMode::AutoNoVsync, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(FrameTimeDiagnosticsPlugin) .add_plugin(FrameTimeDiagnosticsPlugin)

View file

@ -4,18 +4,16 @@
use bevy::prelude::*; use bevy::prelude::*;
fn main() { fn main() {
// Set the window's parameters, note we're setting always_on_top to be true.
let window_desc = WindowDescriptor {
transparent: true,
decorations: true,
always_on_top: true,
..default()
};
App::new() App::new()
.insert_resource(ClearColor(Color::NONE)) // Use a transparent window, to make effects obvious. .insert_resource(ClearColor(Color::NONE)) // Use a transparent window, to make effects obvious.
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: window_desc, primary_window: Some(Window {
// Set the window's parameters, note we're setting the window to always be on top.
transparent: true,
decorations: true,
always_on_top: true,
..default()
}),
..default() ..default()
})) }))
.add_startup_system(setup) .add_startup_system(setup)
@ -51,10 +49,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
)); ));
} }
// A simple system to handle some keyboard input and toggle on/off the hittest. // A simple system to handle some keyboard input and toggle on/off the hittest.
fn toggle_mouse_passthrough(keyboard_input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) { fn toggle_mouse_passthrough(keyboard_input: Res<Input<KeyCode>>, mut windows: Query<&mut Window>) {
if keyboard_input.just_pressed(KeyCode::P) { if keyboard_input.just_pressed(KeyCode::P) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
let hittest: bool = window.hittest(); window.cursor.hit_test = !window.cursor.hit_test;
window.set_cursor_hittest(!hittest);
} }
} }

View file

@ -6,7 +6,7 @@
use bevy::{ use bevy::{
prelude::*, prelude::*,
utils::Duration, utils::Duration,
window::{PresentMode, RequestRedraw}, window::{PresentMode, RequestRedraw, WindowPlugin},
winit::WinitSettings, winit::WinitSettings,
}; };
@ -26,11 +26,11 @@ fn main() {
}) })
.insert_resource(ExampleMode::Game) .insert_resource(ExampleMode::Game)
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
// Turn off vsync to maximize CPU/GPU usage // Turn off vsync to maximize CPU/GPU usage
present_mode: PresentMode::AutoNoVsync, present_mode: PresentMode::AutoNoVsync,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_startup_system(test_setup::setup) .add_startup_system(test_setup::setup)

View file

@ -1,24 +1,17 @@
//! Uses two windows to visualize a 3D model from different angles. //! Uses two windows to visualize a 3D model from different angles.
use bevy::{ use bevy::{prelude::*, render::camera::RenderTarget, window::WindowRef};
prelude::*,
render::camera::RenderTarget,
window::{CreateWindow, PresentMode, WindowId},
};
fn main() { fn main() {
App::new() App::new()
// By default, a primary window gets spawned by `WindowPlugin`, contained in `DefaultPlugins`
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_startup_system(setup) .add_startup_system(setup_scene)
.add_system(bevy::window::close_on_esc) .add_system(bevy::window::close_on_esc)
.run(); .run();
} }
fn setup( fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
mut commands: Commands,
asset_server: Res<AssetServer>,
mut create_window_events: EventWriter<CreateWindow>,
) {
// add entities to the world // add entities to the world
commands.spawn(SceneBundle { commands.spawn(SceneBundle {
scene: asset_server.load("models/monkey/Monkey.gltf#Scene0"), scene: asset_server.load("models/monkey/Monkey.gltf#Scene0"),
@ -29,31 +22,26 @@ fn setup(
transform: Transform::from_xyz(4.0, 5.0, 4.0), transform: Transform::from_xyz(4.0, 5.0, 4.0),
..default() ..default()
}); });
// main camera // main camera, cameras default to the primary window
// so we don't need to specify that.
commands.spawn(Camera3dBundle { commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 0.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y), transform: Transform::from_xyz(0.0, 0.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y),
..default() ..default()
}); });
let window_id = WindowId::new(); // Spawn a second window
let second_window = commands
// sends out a "CreateWindow" event, which will be received by the windowing backend .spawn(Window {
create_window_events.send(CreateWindow { title: "Second window".to_owned(),
id: window_id,
descriptor: WindowDescriptor {
width: 800.,
height: 600.,
present_mode: PresentMode::AutoNoVsync,
title: "Second window".to_string(),
..default() ..default()
}, })
}); .id();
// second window camera // second window camera
commands.spawn(Camera3dBundle { commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(6.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), transform: Transform::from_xyz(6.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
camera: Camera { camera: Camera {
target: RenderTarget::Window(window_id), target: RenderTarget::Window(WindowRef::Entity(second_window)),
..default() ..default()
}, },
..default() ..default()

View file

@ -1,19 +1,19 @@
//! This example illustrates how to override the window scale factor imposed by the //! This example illustrates how to override the window scale factor imposed by the
//! operating system. //! operating system.
use bevy::prelude::*; use bevy::{prelude::*, window::WindowResolution};
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
width: 500., resolution: WindowResolution::new(500., 300.).with_scale_factor_override(1.0),
height: 300.,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_startup_system(setup) .add_startup_system(setup)
.add_system(display_override)
.add_system(toggle_override) .add_system(toggle_override)
.add_system(change_scale_factor) .add_system(change_scale_factor)
.run(); .run();
@ -63,20 +63,39 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
}); });
} }
/// Set the title of the window to the current override
fn display_override(mut windows: Query<&mut Window>) {
let mut window = windows.single_mut();
window.title = format!(
"Scale override: {:?}",
window.resolution.scale_factor_override()
);
}
/// This system toggles scale factor overrides when enter is pressed /// This system toggles scale factor overrides when enter is pressed
fn toggle_override(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) { fn toggle_override(input: Res<Input<KeyCode>>, mut windows: Query<&mut Window>) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
if input.just_pressed(KeyCode::Return) { if input.just_pressed(KeyCode::Return) {
window.set_scale_factor_override(window.scale_factor_override().xor(Some(1.))); let scale_factor_override = window.resolution.scale_factor_override();
window
.resolution
.set_scale_factor_override(scale_factor_override.xor(Some(1.0)));
} }
} }
/// This system changes the scale factor override when up or down is pressed /// This system changes the scale factor override when up or down is pressed
fn change_scale_factor(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) { fn change_scale_factor(input: Res<Input<KeyCode>>, mut windows: Query<&mut Window>) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
let scale_factor_override = window.resolution.scale_factor_override();
if input.just_pressed(KeyCode::Up) { if input.just_pressed(KeyCode::Up) {
window.set_scale_factor_override(window.scale_factor_override().map(|n| n + 1.)); window
.resolution
.set_scale_factor_override(scale_factor_override.map(|n| n + 1.0));
} else if input.just_pressed(KeyCode::Down) { } else if input.just_pressed(KeyCode::Down) {
window.set_scale_factor_override(window.scale_factor_override().map(|n| (n - 1.).max(1.))); window
.resolution
.set_scale_factor_override(scale_factor_override.map(|n| (n - 1.0).max(1.0)));
} }
} }

View file

@ -4,7 +4,10 @@
//! [documentation](https://docs.rs/bevy/latest/bevy/prelude/struct.WindowDescriptor.html#structfield.transparent) //! [documentation](https://docs.rs/bevy/latest/bevy/prelude/struct.WindowDescriptor.html#structfield.transparent)
//! for more details. //! for more details.
use bevy::prelude::*; use bevy::{
prelude::*,
window::{Window, WindowPlugin},
};
fn main() { fn main() {
App::new() App::new()
@ -12,13 +15,13 @@ fn main() {
.insert_resource(ClearColor(Color::NONE)) .insert_resource(ClearColor(Color::NONE))
.add_startup_system(setup) .add_startup_system(setup)
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
// Setting `transparent` allows the `ClearColor`'s alpha value to take effect // Setting `transparent` allows the `ClearColor`'s alpha value to take effect
transparent: true, transparent: true,
// Disabling window decorations to make it feel more like a widget than a window // Disabling window decorations to make it feel more like a widget than a window
decorations: false, decorations: false,
..default() ..default()
}, }),
..default() ..default()
})) }))
.run(); .run();

View file

@ -62,22 +62,22 @@ fn setup_ui(mut cmd: Commands, asset_server: Res<AssetServer>) {
/// This system shows how to request the window to a new resolution /// This system shows how to request the window to a new resolution
fn toggle_resolution( fn toggle_resolution(
keys: Res<Input<KeyCode>>, keys: Res<Input<KeyCode>>,
mut windows: ResMut<Windows>, mut windows: Query<&mut Window>,
resolution: Res<ResolutionSettings>, resolution: Res<ResolutionSettings>,
) { ) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
if keys.just_pressed(KeyCode::Key1) { if keys.just_pressed(KeyCode::Key1) {
let res = resolution.small; let res = resolution.small;
window.set_resolution(res.x, res.y); window.resolution.set(res.x, res.y);
} }
if keys.just_pressed(KeyCode::Key2) { if keys.just_pressed(KeyCode::Key2) {
let res = resolution.medium; let res = resolution.medium;
window.set_resolution(res.x, res.y); window.resolution.set(res.x, res.y);
} }
if keys.just_pressed(KeyCode::Key3) { if keys.just_pressed(KeyCode::Key3) {
let res = resolution.large; let res = resolution.large;
window.set_resolution(res.x, res.y); window.resolution.set(res.x, res.y);
} }
} }

View file

@ -10,14 +10,14 @@ use bevy::{
fn main() { fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
title: "I am a window!".to_string(), title: "I am a window!".into(),
width: 500., resolution: (500., 300.).into(),
height: 300.,
present_mode: PresentMode::AutoVsync, present_mode: PresentMode::AutoVsync,
always_on_top: true, // Tells wasm to resize the window according to the available canvas
fit_canvas_to_parent: true,
..default() ..default()
}, }),
..default() ..default()
})) }))
.add_plugin(LogDiagnosticsPlugin::default()) .add_plugin(LogDiagnosticsPlugin::default())
@ -32,16 +32,16 @@ fn main() {
/// This system toggles the vsync mode when pressing the button V. /// This system toggles the vsync mode when pressing the button V.
/// You'll see fps increase displayed in the console. /// You'll see fps increase displayed in the console.
fn toggle_vsync(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) { fn toggle_vsync(input: Res<Input<KeyCode>>, mut windows: Query<&mut Window>) {
if input.just_pressed(KeyCode::V) { if input.just_pressed(KeyCode::V) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
window.set_present_mode(if matches!(window.present_mode(), PresentMode::AutoVsync) { window.present_mode = if matches!(window.present_mode, PresentMode::AutoVsync) {
PresentMode::AutoNoVsync PresentMode::AutoNoVsync
} else { } else {
PresentMode::AutoVsync PresentMode::AutoVsync
}); };
info!("PRESENT_MODE: {:?}", window.present_mode()); info!("PRESENT_MODE: {:?}", window.present_mode);
} }
} }
@ -51,49 +51,49 @@ fn toggle_vsync(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) {
/// This feature only works on some platforms. Please check the /// This feature only works on some platforms. Please check the
/// [documentation](https://docs.rs/bevy/latest/bevy/prelude/struct.WindowDescriptor.html#structfield.always_on_top) /// [documentation](https://docs.rs/bevy/latest/bevy/prelude/struct.WindowDescriptor.html#structfield.always_on_top)
/// for more details. /// for more details.
fn toggle_always_on_top(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) { fn toggle_always_on_top(input: Res<Input<KeyCode>>, mut windows: Query<&mut Window>) {
if input.just_pressed(KeyCode::T) { if input.just_pressed(KeyCode::T) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
let on_top: bool = window.always_on_top(); window.always_on_top = !window.always_on_top;
if on_top { if window.always_on_top {
info!("UNLOCKING WINDOW");
} else {
info!("LOCKING WINDOW ON TOP"); info!("LOCKING WINDOW ON TOP");
} else {
info!("UNLOCKING WINDOW");
} }
window.set_always_on_top(!on_top);
} }
} }
/// This system will then change the title during execution /// This system will then change the title during execution
fn change_title(time: Res<Time>, mut windows: ResMut<Windows>) { fn change_title(mut windows: Query<&mut Window>, time: Res<Time>) {
let window = windows.primary_mut(); let mut window = windows.single_mut();
window.set_title(format!( window.title = format!(
"Seconds since startup: {}", "Seconds since startup: {}",
time.elapsed_seconds().round() time.elapsed().as_secs_f32().round()
)); );
} }
/// This system toggles the cursor's visibility when the space bar is pressed fn toggle_cursor(mut windows: Query<&mut Window>, input: Res<Input<KeyCode>>) {
fn toggle_cursor(input: Res<Input<KeyCode>>, mut windows: ResMut<Windows>) {
let window = windows.primary_mut();
if input.just_pressed(KeyCode::Space) { if input.just_pressed(KeyCode::Space) {
window.set_cursor_grab_mode(match window.cursor_grab_mode() { let mut window = windows.single_mut();
window.cursor.visible = !window.cursor.visible;
window.cursor.grab_mode = match window.cursor.grab_mode {
CursorGrabMode::None => CursorGrabMode::Locked, CursorGrabMode::None => CursorGrabMode::Locked,
CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None, CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
}); };
window.set_cursor_visibility(!window.cursor_visible());
} }
} }
/// This system cycles the cursor's icon through a small set of icons when clicking /// This system cycles the cursor's icon through a small set of icons when clicking
fn cycle_cursor_icon( fn cycle_cursor_icon(
mut windows: Query<&mut Window>,
input: Res<Input<MouseButton>>, input: Res<Input<MouseButton>>,
mut windows: ResMut<Windows>,
mut index: Local<usize>, mut index: Local<usize>,
) { ) {
let mut window = windows.single_mut();
const ICONS: &[CursorIcon] = &[ const ICONS: &[CursorIcon] = &[
CursorIcon::Default, CursorIcon::Default,
CursorIcon::Hand, CursorIcon::Hand,
@ -101,16 +101,16 @@ fn cycle_cursor_icon(
CursorIcon::Text, CursorIcon::Text,
CursorIcon::Copy, CursorIcon::Copy,
]; ];
let window = windows.primary_mut();
if input.just_pressed(MouseButton::Left) { if input.just_pressed(MouseButton::Left) {
*index = (*index + 1) % ICONS.len(); *index = (*index + 1) % ICONS.len();
window.set_cursor_icon(ICONS[*index]);
} else if input.just_pressed(MouseButton::Right) { } else if input.just_pressed(MouseButton::Right) {
*index = if *index == 0 { *index = if *index == 0 {
ICONS.len() - 1 ICONS.len() - 1
} else { } else {
*index - 1 *index - 1
}; };
window.set_cursor_icon(ICONS[*index]);
} }
window.cursor.icon = ICONS[*index];
} }

View file

@ -7,10 +7,10 @@ fn main() {
// it is currently. // it is currently.
App::new() App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor { primary_window: Some(Window {
title: "Minimising".into(), title: "Minimising".into(),
..Default::default() ..default()
}, }),
..default() ..default()
})) }))
.add_system(minimise_automatically) .add_system(minimise_automatically)
@ -19,9 +19,10 @@ fn main() {
.run(); .run();
} }
fn minimise_automatically(mut windows: ResMut<Windows>, mut frames: Local<u32>) { fn minimise_automatically(mut windows: Query<&mut Window>, mut frames: Local<u32>) {
if *frames == 60 { if *frames == 60 {
windows.primary_mut().set_minimized(true); let mut window = windows.single_mut();
window.set_minimized(true);
} else { } else {
*frames += 1; *frames += 1;
} }

View file

@ -1,7 +1,7 @@
//! A test to confirm that `bevy` allows setting the window to arbitrary small sizes //! A test to confirm that `bevy` allows setting the window to arbitrary small sizes
//! This is run in CI to ensure that this doesn't regress again. //! This is run in CI to ensure that this doesn't regress again.
use bevy::{core_pipeline::clear_color::ClearColorConfig, prelude::*}; use bevy::{core_pipeline::clear_color::ClearColorConfig, prelude::*, window::WindowResolution};
// The smallest size reached is 1x1, as X11 doesn't support windows with a 0 dimension // The smallest size reached is 1x1, as X11 doesn't support windows with a 0 dimension
// TODO: Add a check for platforms other than X11 for 0xk and kx0, despite those currently unsupported on CI. // TODO: Add a check for platforms other than X11 for 0xk and kx0, despite those currently unsupported on CI.
@ -23,16 +23,17 @@ fn main() {
width: MAX_WIDTH, width: MAX_WIDTH,
height: MAX_HEIGHT, height: MAX_HEIGHT,
}) })
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(
window: WindowDescriptor { DefaultPlugins.set(WindowPlugin {
width: MAX_WIDTH.try_into().unwrap(), primary_window: Some(Window {
height: MAX_HEIGHT.try_into().unwrap(), resolution: WindowResolution::new(MAX_WIDTH as f32, MAX_HEIGHT as f32)
scale_factor_override: Some(1.), .with_scale_factor_override(1.0),
title: "Resizing".into(), title: "Resizing".into(),
..Default::default() ..default()
}, }),
..default() ..default()
})) }),
)
.insert_resource(Phase::ContractingY) .insert_resource(Phase::ContractingY)
.add_system(change_window_size) .add_system(change_window_size)
.add_system(sync_dimensions) .add_system(sync_dimensions)
@ -98,12 +99,10 @@ fn change_window_size(
} }
} }
fn sync_dimensions(dim: Res<Dimensions>, mut windows: ResMut<Windows>) { fn sync_dimensions(dim: Res<Dimensions>, mut windows: Query<&mut Window>) {
if dim.is_changed() { if dim.is_changed() {
windows.primary_mut().set_resolution( let mut window = windows.single_mut();
dim.width.try_into().unwrap(), window.resolution.set(dim.width as f32, dim.height as f32);
dim.height.try_into().unwrap(),
);
} }
} }