diff --git a/crates/bevy_window2/Cargo.toml b/crates/bevy_window2/Cargo.toml new file mode 100644 index 0000000000000..3f7d5e90b747c --- /dev/null +++ b/crates/bevy_window2/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "bevy_window2" +version = "0.8.0-dev" +edition = "2021" +description = "Provides windowing functionality for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.8.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.8.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.8.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.8.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.8.0-dev" } +raw-window-handle = "0.4.2" + +# other + +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = "0.3" diff --git a/crates/bevy_window2/src/commands.rs b/crates/bevy_window2/src/commands.rs new file mode 100644 index 0000000000000..69d95c090884c --- /dev/null +++ b/crates/bevy_window2/src/commands.rs @@ -0,0 +1,271 @@ +use bevy_ecs::{ + entity::Entity, + event::Events, + prelude::World, + system::{Command, Commands}, +}; +use bevy_math::{IVec2, Vec2}; + +use crate::{CursorIcon, WindowMode, WindowPresentMode, WindowResizeConstraints}; + +/// Window commands sent to window backends +#[derive(Debug)] +pub enum WindowCommand { + /// Set window mode and resolution + SetWindowMode(WindowMode, (u32, u32)), + /// Set window title + SetTitle(String), + /// Set window scale factor + SetScaleFactor(f64), + /// Set window resolution and scale factor + SetResolution((f32, f32), f64), + /// Set window present mode + SetPresentMode(WindowPresentMode), + /// Set whether window is resizeable + SetResizable(bool), + /// Set whether window has decorations + SetDecorations(bool), + /// Set cursor icon + SetCursorIcon(CursorIcon), + /// Set whether the cursor will be locked + SetCursorLockMode(bool), + /// Set whether the cursor will be visible + SetCursorVisibility(bool), + /// Set cursor position + SetCursorPosition(Vec2), + /// Sets the window to maximized or back + SetMaximized(bool), + /// Sets the window to minimized or back + SetMinimized(bool), + /// Set window position + SetPosition(IVec2), + /// Set window resize constraints + SetResizeConstraints(WindowResizeConstraints), +} + +/// An event that is sent when window commands have been queued +#[derive(Debug)] +pub struct WindowCommandQueued { + /// Window id + pub window_id: Entity, + /// Queued command + pub command: WindowCommand, +} + +impl Command for WindowCommandQueued { + fn write(self, world: &mut World) { + let mut events = world + .get_resource_mut::>() + .unwrap(); + events.send(self); + } +} + +/// A list of commands that will be run to modify a window. +pub struct WindowCommands<'w, 's, 'a> { + window_id: Entity, + commands: &'a mut Commands<'w, 's>, +} + +impl<'w, 's, 'a> WindowCommands<'w, 's, 'a> { + /// Adds a window command directly to the command list. + #[inline] + pub fn add(&mut self, command: WindowCommand) -> &mut Self { + self.commands.add(WindowCommandQueued { + window_id: self.window_id, + command, + }); + self + } + + /// Set window display mode + #[inline] + pub fn set_window_mode(&mut self, mode: WindowMode, resolution: (u32, u32)) -> &mut Self { + self.add(WindowCommand::SetWindowMode(mode, resolution)) + } + + /// Set window title + #[inline] + pub fn set_title(&mut self, title: String) -> &mut Self { + self.add(WindowCommand::SetTitle(title)) + } + + /// Set window scale factor + #[inline] + pub fn set_scale_factor(&mut self, scale_factor: f64) -> &mut Self { + self.add(WindowCommand::SetScaleFactor(scale_factor)) + } + + /// Set window resolution + #[inline] + pub fn set_resolution( + &mut self, + logical_resolution: (f32, f32), + scale_factor: f64, + ) -> &mut Self { + self.add(WindowCommand::SetResolution( + logical_resolution, + scale_factor, + )) + } + + /// Set window present mode + #[inline] + #[doc(alias = "set_vsync")] + pub fn set_present_mode(&mut self, present_mode: WindowPresentMode) -> &mut Self { + self.add(WindowCommand::SetPresentMode(present_mode)) + } + + /// Set whether the window can resize + #[inline] + pub fn set_resizable(&mut self, resizable: bool) -> &mut Self { + self.add(WindowCommand::SetResizable(resizable)) + } + + /// Set whether the window should have decorations, e.g. borders, title bar + #[inline] + pub fn set_decorations(&mut self, decorations: bool) -> &mut Self { + self.add(WindowCommand::SetDecorations(decorations)) + } + + /// Set window icon + #[inline] + pub fn set_cursor_icon(&mut self, icon: CursorIcon) -> &mut Self { + self.add(WindowCommand::SetCursorIcon(icon)) + } + + /// Set whether the cursor will be locked + #[inline] + pub fn set_cursor_lock_mode(&mut self, locked: bool) -> &mut Self { + self.add(WindowCommand::SetCursorLockMode(locked)) + } + + /// Set whether the cursor will be visible + #[inline] + pub fn set_cursor_visibility(&mut self, visible: bool) -> &mut Self { + self.add(WindowCommand::SetCursorVisibility(visible)) + } + + /// Set cursor position + #[inline] + pub fn set_cursor_position(&mut self, position: Vec2) -> &mut Self { + self.add(WindowCommand::SetCursorPosition(position)) + } + + /// Sets the window to maximized or back + #[inline] + pub fn set_maximized(&mut self, maximized: bool) -> &mut Self { + self.add(WindowCommand::SetMaximized(maximized)) + } + + /// Sets the window to minimized or back + /// + /// # Platform-specific + /// - iOS / Android / Web: Unsupported. + /// - Wayland: Un-minimize is unsupported. + #[inline] + pub fn set_minimized(&mut self, minimized: bool) -> &mut Self { + self.add(WindowCommand::SetMinimized(minimized)) + } + + /// Modifies the position of the window in physical pixels. + /// + /// Note that the top-left hand corner of the desktop is not necessarily the same as the screen. + /// If the user uses a desktop with multiple monitors, the top-left hand corner of the + /// desktop is the top-left hand corner of the monitor at the top-left of the desktop. This + /// automatically un-maximizes the window if it's maximized. + /// + /// # Platform-specific + /// + /// - iOS: Can only be called on the main thread. Sets the top left coordinates of the window in + /// the screen space coordinate system. + /// - Web: Sets the top-left coordinates relative to the viewport. + /// - Android / Wayland: Unsupported. + #[inline] + pub fn set_position(&mut self, position: IVec2) -> &mut Self { + self.add(WindowCommand::SetPosition(position)) + } + + /// Set window resize constraints + /// + /// Modifies the minimum and maximum window bounds for resizing in logical pixels. + #[inline] + pub fn set_resize_constraints( + &mut self, + resize_constraints: WindowResizeConstraints, + ) -> &mut Self { + self.add(WindowCommand::SetResizeConstraints(resize_constraints)) + } +} + +/// Extension trait for [`Commands`], adding a [`WindowCommands`] helper +pub trait CommandsExt<'w, 's> { + /// Returns an [`WindowCommands`] builder for the requested window. + fn window<'a>(&'a mut self, window: Entity) -> WindowCommands<'w, 's, 'a>; +} + +impl<'w, 's> CommandsExt<'w, 's> for Commands<'w, 's> { + #[track_caller] + fn window<'a>(&'a mut self, window_id: Entity) -> WindowCommands<'w, 's, 'a> { + // currently there is no way of checking if an entity exists from `Commands` + // some function like `exists` or `contains` should get added to `Commands` + WindowCommands { + window_id, + commands: self, + } + } +} + +#[cfg(test)] +mod tests { + use bevy_ecs::{ + event::{EventReader, Events}, + prelude::World, + schedule::{Stage, SystemStage}, + system::Commands, + }; + use bevy_math::IVec2; + + use crate::{CommandsExt, WindowCommandQueued}; + + fn create_commands(mut commands: Commands) { + let first = commands.spawn().id(); + let mut first_cmds = commands.window(first); + first_cmds + .set_cursor_lock_mode(false) + .set_title("first".to_string()) + .set_position(IVec2::ONE); + + let second = commands.spawn().id(); + let mut second_cmds = commands.window(second); + second_cmds + .set_cursor_lock_mode(true) + .set_title("second".to_string()) + .set_position(IVec2::ZERO); + } + + fn receive_commands(mut events: EventReader) { + let received = events + .iter() + .map(|WindowCommandQueued { window_id, command }| format!("{window_id:?}: {command:?}")) + .collect::>(); + let expected = vec![ + "0v0: SetCursorLockMode(false)".to_string(), + "0v0: SetTitle(\"first\")".to_string(), + "0v0: SetPosition(IVec2(1, 1))".to_string(), + "1v0: SetCursorLockMode(true)".to_string(), + "1v0: SetTitle(\"second\")".to_string(), + "1v0: SetPosition(IVec2(0, 0))".to_string(), + ]; + assert_eq!(received, expected); + } + + #[test] + fn test_window_commands() { + let mut world = World::new(); + world.init_resource::>(); + + SystemStage::single(create_commands).run(&mut world); + SystemStage::single(receive_commands).run(&mut world); + } +} diff --git a/crates/bevy_window2/src/cursor.rs b/crates/bevy_window2/src/cursor.rs new file mode 100644 index 0000000000000..41c6992a9cbce --- /dev/null +++ b/crates/bevy_window2/src/cursor.rs @@ -0,0 +1,78 @@ +use bevy_reflect::Reflect; + +/// The icon to display for a window's cursor +#[derive(Reflect, Debug, Clone, Copy, PartialEq, Eq)] +pub enum CursorIcon { + /// The platform-dependent default cursor. + Default, + /// A simple crosshair. + Crosshair, + /// A hand (often used to indicate links in web browsers). + Hand, + /// An arrow. This is the default cursor on most systems. + Arrow, + /// Indicates something is to be moved. + Move, + /// Indicates text that may be selected or edited. + Text, + /// Program busy indicator. + Wait, + /// Help indicator (often rendered as a "?") + Help, + /// Progress indicator. Shows that processing is being done. But in contrast + /// with "Wait" the user may still interact with the program. Often rendered + /// as a spinning beach ball, or an arrow with a watch or hourglass. + Progress, + /// Cursor showing that something cannot be done. + NotAllowed, + /// Indicates that a context menu is available + ContextMenu, + /// Indicates that a cell (or set of cells) may be selected + Cell, + /// Indicates vertical text that may be selected or edited + VerticalText, + /// Indicates that an alias of something is to be created + Alias, + /// Indicates something is to be copied + Copy, + /// Indicates that the dragged item cannot be dropped here + NoDrop, + /// Indicates that something can be grabbed + Grab, + /// Indicates that something is grabbed. + Grabbing, + /// Indicates that the user can scroll by dragging the mouse. + AllScroll, + /// Indicates that the user can zoom in + ZoomIn, + /// Indicates that the user can zoom out + ZoomOut, + /// Indicates that an edge of a box is to be moved right (east) + EResize, + /// Indicates that an edge of a box is to be moved up (north) + NResize, + /// Indicates that an edge of a box is to be moved up and right (north/east) + NeResize, + /// indicates that an edge of a box is to be moved up and left (north/west) + NwResize, + /// Indicates that an edge of a box is to be moved down (south) + SResize, + /// The cursor indicates that an edge of a box is to be moved down and right (south/east) + SeResize, + /// The cursor indicates that an edge of a box is to be moved down and left (south/west) + SwResize, + /// Indicates that an edge of a box is to be moved left (west) + WResize, + /// Indicates a bidirectional resize cursor + EwResize, + /// Indicates a bidirectional resize cursor + NsResize, + /// Indicates a bidirectional resize cursor + NeswResize, + /// Indicates a bidirectional resize cursor + NwseResize, + /// Indicates that a column can be resized horizontally + ColResize, + /// Indicates that the row can be resized vertically + RowResize, +} diff --git a/crates/bevy_window2/src/events.rs b/crates/bevy_window2/src/events.rs new file mode 100644 index 0000000000000..321cf639a2dd4 --- /dev/null +++ b/crates/bevy_window2/src/events.rs @@ -0,0 +1,122 @@ +use std::path::PathBuf; + +use bevy_ecs::entity::Entity; +use bevy_math::{IVec2, Vec2}; + +/// An event that is sent when the cursor has entered a window +#[derive(Debug, Clone)] +pub struct CursorEntered { + /// Window id + pub window_id: Entity, +} + +/// An event that is sent when the cursor has left a window +#[derive(Debug, Clone)] +pub struct CursorLeft { + /// Window id + pub window_id: Entity, +} + +/// An event that is sent when the cursor in a window has moved +#[derive(Debug, Clone)] +pub struct CursorMoved { + /// Window id + pub window_id: Entity, + /// The new position of cursor + pub position: Vec2, +} + +/// Events related to files being dragged and dropped on a window. +#[derive(Debug, Clone)] +pub enum FileDragAndDrop { + /// An event that is sent when a file has been dropped over a window + DroppedFile { + /// Window id + window_id: Entity, + /// Path of dropped file + path_buf: PathBuf, + }, + + /// An event that is sent when a file is hovering over a window + HoveredFile { + /// Window id + window_id: Entity, + /// Path of hovered file + path_buf: PathBuf, + }, + + /// An event that is sent when a file is no longer hovering over a window + HoveredFileCancelled { + /// Window id + window_id: Entity, + }, +} + +/// An event that is sent whenever a window receives a character from the OS or underlying system. +#[derive(Debug, Clone)] +pub struct ReceivedCharacter { + /// Window id + pub window_id: Entity, + /// Received character + pub char: char, +} + +/// An event that indicates the window should redraw, even if its control flow is set to `Wait` and +/// there have been no window events. +#[derive(Debug, Clone)] +pub struct RequestRedraw; + +/// An event that is sent whenever a close was requested for a window. For example: when the "close" +/// button is pressed on a window. +#[derive(Debug, Clone)] +pub struct WindowCloseRequested { + /// Window id + pub window_id: Entity, +} + +/// An event that indicates a window has received or lost focus. +#[derive(Debug, Clone)] +pub struct WindowFocused { + /// Window id + pub window_id: Entity, + /// Whether window has received or lost focus + pub focused: bool, +} + +/// An event that is sent when a window is repositioned in physical pixels. +#[derive(Debug, Clone)] +pub struct WindowMoved { + /// Window id + pub window_id: Entity, + /// The new position of the window + pub position: IVec2, +} + +/// A window event that is sent whenever a windows logical size has changed +#[derive(Debug, Clone)] +pub struct WindowResized { + /// Window id + pub window_id: Entity, + /// The new logical width of the window + pub width: f32, + /// The new logical height of the window + pub height: f32, +} + +/// An event that indicates a window's scale factor has changed. +#[derive(Debug, Clone)] +pub struct WindowScaleFactorChanged { + /// Window id + pub window_id: Entity, + /// The new window scale factor + pub scale_factor: f64, +} + +/// An event that indicates a window's OS-reported scale factor has changed. +#[derive(Debug, Clone)] +pub struct WindowScaleFactorBackendChanged { + /// Window id + pub window_id: Entity, + /// The new window scale factor + pub scale_factor: f64, +} diff --git a/crates/bevy_window2/src/exit.rs b/crates/bevy_window2/src/exit.rs new file mode 100644 index 0000000000000..e91149754d6e2 --- /dev/null +++ b/crates/bevy_window2/src/exit.rs @@ -0,0 +1,38 @@ +use crate::{PrimaryWindow, Window}; + +use bevy_app::AppExit; +use bevy_ecs::{ + event::EventWriter, + system::{Query, Res}, +}; + +/// Exit condition +pub enum ExitCondition { + /// Exit app when all windows are closed + OnAllClosed, + /// Exit app when the primary window is closed + OnPrimaryClosed, + /// Stay headless even if all windows are closed + DontExit, +} + +/// system for [`ExitCondition::OnAllClosed`] +pub fn exit_on_all_window_closed_system( + mut app_exit_events: EventWriter, + windows: Query<&Window>, +) { + if windows.is_empty() { + app_exit_events.send(AppExit); + } +} + +/// system for [`ExitCondition::OnPrimaryClosed`] +pub fn exit_on_primary_window_closed_system( + mut app_exit_events: EventWriter, + windows: Query<&Window>, + primary_window: Res, +) { + if windows.get(**primary_window).is_err() { + app_exit_events.send(AppExit); + } +} diff --git a/crates/bevy_window2/src/lib.rs b/crates/bevy_window2/src/lib.rs new file mode 100644 index 0000000000000..b20ee555494bb --- /dev/null +++ b/crates/bevy_window2/src/lib.rs @@ -0,0 +1,74 @@ +#![deny(missing_docs)] +//! Windows for the game engine + +mod commands; +mod cursor; +mod events; +mod exit; +mod window; + +pub use commands::*; +pub use cursor::*; +pub use events::*; +pub use exit::*; +pub use window::*; + +use bevy_app::prelude::*; + +/// Adds support for a Bevy Application to create windows +pub struct WindowPlugin { + /// Create a primary window automatically + /// + /// If a [`WindowDescriptor`] resource is inserted before this plugin loaded, + /// the primary window will use that descriptor + pub add_primary_window: bool, + /// Condition for app to exit + pub exit_condition: ExitCondition, +} + +impl Default for WindowPlugin { + fn default() -> Self { + WindowPlugin { + add_primary_window: true, + // should this default to OnAllClosed or OnPrimaryClosed + exit_condition: ExitCondition::OnAllClosed, + } + } +} + +impl Plugin for WindowPlugin { + fn build(&self, app: &mut App) { + app.add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::() + .add_event::(); + + if self.add_primary_window { + let descriptor = app + .world + .get_resource::() + .map(|descriptor| (*descriptor).clone()) + .unwrap_or_default(); + let window = app.world.spawn().insert(descriptor).id(); + app.insert_resource(PrimaryWindow(window)); + } + + match self.exit_condition { + ExitCondition::OnAllClosed => { + app.add_system(exit_on_all_window_closed_system); + } + ExitCondition::OnPrimaryClosed => { + app.add_system(exit_on_primary_window_closed_system); + } + _ => {} + } + } +} diff --git a/crates/bevy_window2/src/window.rs b/crates/bevy_window2/src/window.rs new file mode 100644 index 0000000000000..49763aea89cca --- /dev/null +++ b/crates/bevy_window2/src/window.rs @@ -0,0 +1,373 @@ +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::prelude::{Component, Entity}; +use bevy_math::{DVec2, IVec2, Vec2}; +use bevy_reflect::Reflect; +use bevy_utils::tracing::warn; +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; + +use crate::{CursorIcon, WindowCommand}; + +/// Resource containing entity id of the primary window +#[derive(Reflect, Debug, Deref, DerefMut, Clone, Copy, PartialEq)] +pub struct PrimaryWindow(pub Entity); + +/// Resource containing entity id of the focused window +#[derive(Reflect, Debug, Deref, DerefMut, Clone, Copy, PartialEq)] +pub struct FocusedWindow(pub Entity); + +/// Marker for windows that have created by the backend +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct Window; + +/// Marker for the window that is currently focused +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct WindowIsFocused; + +/// Marker for windows that have decorations enabled +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct WindowDecorated; + +/// Marker for windows that are resizeable +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct WindowResizable; + +/// Component that describes how the window should be created +#[derive(Component, Reflect, Debug, Clone)] +pub struct WindowDescriptor { + /// Sets the starting width + pub width: f32, + /// Sets the starting height + pub height: f32, + /// Sets the starting position + pub position: Option, + /// Sets the resize constraints + pub resize_constraints: WindowResizeConstraints, + /// Override the scale factor + pub scale_factor_override: Option, + /// Sets the window title + pub title: String, + /// Sets the window present mode + #[doc(alias = "vsync")] + pub present_mode: WindowPresentMode, + /// Sets whether the window can resize + pub resizable: bool, + /// Sets whether the window should enable decorations, e.g. border, title bar, etc + pub decorations: bool, + /// Sets whether the window should hide the touse cursor + pub cursor_visible: bool, + /// Sets whether the window should grab the mouse cursor + pub cursor_locked: bool, + /// Sets the display mode of the window + pub mode: WindowMode, + /// Sets whether the background of the window should be transparent. + /// # Platform-specific + /// - iOS / Android / Web: Unsupported. + /// - macOS X: Not working as expected. + /// - Windows 11: Not working as expected + /// macOS X transparent works with winit out of the box, so this issue might be related to: + /// Windows 11 is related to + pub transparent: bool, + /// Sets the selector used to find locate the element, + /// on `None` a new canvas will be appended to the document body + #[cfg(target_arch = "wasm32")] + pub canvas: Option, +} + +impl Default for WindowDescriptor { + fn default() -> Self { + WindowDescriptor { + title: "app".to_string(), + width: 1280., + height: 720., + position: None, + resize_constraints: WindowResizeConstraints::default(), + scale_factor_override: None, + present_mode: WindowPresentMode::Fifo, + resizable: true, + decorations: true, + cursor_locked: false, + cursor_visible: true, + mode: WindowMode::Windowed, + transparent: false, + #[cfg(target_arch = "wasm32")] + canvas: None, + } + } +} + +/// Window canvas +#[cfg(target_arch = "wasm32")] +#[derive(Component, Reflect, Debug, Clone, PartialEq)] +pub struct WindowCanvas(pub String); + +/// Window cursor +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct WindowCursor { + /// Cursor icon + pub icon: CursorIcon, + /// Cursor lock/grab state + pub locked: bool, + /// Cursor visibility + pub visible: bool, +} + +/// Window cursor position +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct WindowCursorPosition(pub DVec2); + +/// Window handle +#[derive(Component, Debug, Clone, Copy, PartialEq)] +pub struct WindowHandle(pub RawWindowHandle); + +unsafe impl Send for WindowHandle {} +unsafe impl Sync for WindowHandle {} +unsafe impl HasRawWindowHandle for WindowHandle { + fn raw_window_handle(&self) -> RawWindowHandle { + self.0 + } +} + +/// Window display mode +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub enum WindowMode { + /// Creates a window that uses the given size + Windowed, + /// Creates a borderless window that uses the full size of the screen + BorderlessFullscreen, + /// Creates a fullscreen window that will render at desktop resolution. The app will use the closest supported size + /// from the given size and scale it to fit the screen. + SizedFullscreen, + /// Creates a fullscreen window that uses the maximum supported size + Fullscreen, +} + +/// Window position +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct WindowPosition(pub IVec2); + +/// Window presentation mode +/// +/// The presentation mode specifies when a frame is presented to the window. The `Fifo` +/// option corresponds to a traditional `VSync`, where the framerate is capped by the +/// display refresh rate. Both `Immediate` and `Mailbox` are low-latency and are not +/// capped by the refresh rate, but may not be available on all platforms. Tearing +/// may be observed with `Immediate` mode, but will not be observed with `Mailbox` or +/// `Fifo`. +/// +/// `Immediate` or `Mailbox` will gracefully fallback to `Fifo` when unavailable. +/// +/// The presentation mode may be declared in the [`WindowDescriptor`](WindowDescriptor::present_mode) +/// or updated using [`WindowCommands`](crate::WindowCommands::set_present_mode). +#[repr(C)] +#[derive(Component, Reflect, Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[doc(alias = "vsync")] +pub enum WindowPresentMode { + /// The presentation engine does **not** wait for a vertical blanking period and + /// the request is presented immediately. This is a low-latency presentation mode, + /// but visible tearing may be observed. Will fallback to `Fifo` if unavailable on the + /// selected platform and backend. Not optimal for mobile. + Immediate = 0, + /// The presentation engine waits for the next vertical blanking period to update + /// the current image, but frames may be submitted without delay. This is a low-latency + /// presentation mode and visible tearing will **not** be observed. Will fallback to `Fifo` + /// if unavailable on the selected platform and backend. Not optimal for mobile. + Mailbox = 1, + /// The presentation engine waits for the next vertical blanking period to update + /// the current image. The framerate will be capped at the display refresh rate, + /// corresponding to the `VSync`. Tearing cannot be observed. Optimal for mobile. + Fifo = 2, // NOTE: The explicit ordinal values mirror wgpu and the vulkan spec. +} + +/// Window resize constraints +/// +/// The size limits on a window. +/// These values are measured in logical pixels, so the user's +/// scale factor does affect the size limits on the window. +/// Please note that if the window is resizable, then when the window is +/// maximized it may have a size outside of these limits. The functionality +/// required to disable maximizing is not yet exposed by winit. +#[derive(Component, Reflect, Debug, Clone, Copy)] +pub struct WindowResizeConstraints { + /// Minimum resize width + pub min_width: f32, + /// Minimum resize height + pub min_height: f32, + /// Maximum resize width + pub max_width: f32, + /// Maximum resize height + pub max_height: f32, +} + +impl Default for WindowResizeConstraints { + fn default() -> Self { + Self { + min_width: 180., + min_height: 120., + max_width: f32::INFINITY, + max_height: f32::INFINITY, + } + } +} + +impl WindowResizeConstraints { + /// Check if constraints are valid + #[must_use] + pub fn check_constraints(&self) -> Self { + let WindowResizeConstraints { + mut min_width, + mut min_height, + mut max_width, + mut max_height, + } = self; + min_width = min_width.max(1.); + min_height = min_height.max(1.); + if max_width < min_width { + warn!( + "The given maximum width {} is smaller than the minimum width {}", + max_width, min_width + ); + max_width = min_width; + } + if max_height < min_height { + warn!( + "The given maximum height {} is smaller than the minimum height {}", + max_height, min_height + ); + max_height = min_height; + } + WindowResizeConstraints { + min_width, + min_height, + max_width, + max_height, + } + } +} + +/// Window resolution +#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq)] +pub struct WindowResolution { + physical_width: u32, + physical_height: u32, + requested_width: f32, + requested_height: f32, + scale_factor_override: Option, + scale_factor_backend: f64, +} + +impl WindowResolution { + /// The current logical width of the window's client area. + #[inline] + pub fn logical_width(&self) -> f32 { + (self.physical_width as f64 / self.scale_factor()) as f32 + } + + /// The current logical height of the window's client area. + #[inline] + pub fn logical_height(&self) -> f32 { + (self.physical_height as f64 / self.scale_factor()) as f32 + } + + /// The window's client area width in physical pixels. + #[inline] + pub fn physical_width(&self) -> u32 { + self.physical_width + } + + /// The window's client area height in physical pixels. + #[inline] + pub fn physical_height(&self) -> u32 { + self.physical_height + } + + /// The requested window client area width in logical pixels from window + /// creation or the last call to [`set_resolution`](crate::WindowCommands::set_resolution). + /// + /// This may differ from the actual width depending on OS size limits and + /// the scaling factor for high DPI monitors. + #[inline] + pub fn requested_width(&self) -> f32 { + self.requested_width + } + + /// The requested window client area height in logical pixels from window + /// creation or the last call to [`set_resolution`](crate::WindowCommands::set_resolution). + /// + /// This may differ from the actual width depending on OS size limits and + /// the scaling factor for high DPI monitors. + #[inline] + pub fn requested_height(&self) -> f32 { + self.requested_height + } + + /// The ratio of physical pixels to logical pixels + /// + /// `physical_pixels = logical_pixels * scale_factor` + pub fn scale_factor(&self) -> f64 { + self.scale_factor_override + .unwrap_or(self.scale_factor_backend) + } + + /// The window scale factor as reported by the window backend. + /// This value is unaffected by [`Self::set_scale_factor_override`]. + #[inline] + pub fn scale_factor_backend(&self) -> f64 { + self.scale_factor_backend + } + /// Request the OS to resize the window such the the client area matches the + /// specified width and height. + /// + /// Call [`Commands::add`](bevy_ecs::system::Commands::add) with returned command if some + #[allow(clippy::float_cmp)] + pub fn set_resolution(&mut self, width: f32, height: f32) -> Option { + if self.requested_width == width && self.requested_height == height { + return None; + } + + self.requested_width = width; + self.requested_height = height; + + Some(WindowCommand::SetResolution( + (self.requested_width, self.requested_height), + self.scale_factor(), + )) + } + + /// Override the os-reported scaling factor + /// + /// Call [`Commands::add`](bevy_ecs::system::Commands::add) on the two returned command if some + #[allow(clippy::float_cmp)] + pub fn set_scale_factor_override( + &mut self, + scale_factor: Option, + ) -> Option<[WindowCommand; 2]> { + if self.scale_factor_override == scale_factor { + return None; + } + + self.scale_factor_override = scale_factor; + + Some([ + WindowCommand::SetScaleFactor(self.scale_factor()), + WindowCommand::SetResolution( + (self.requested_width, self.requested_height), + self.scale_factor(), + ), + ]) + } + + /// Update physical size, should only be called by a window backend + pub fn update_physical_size_from_backend(&mut self, width: u32, height: u32) { + self.physical_width = width; + self.physical_height = height; + } + + /// Update backend scale factor, should only be called by a window backend + pub fn update_scale_factor_from_backend(&mut self, scale_factor: f64) { + self.scale_factor_backend = scale_factor; + } +} + +/// Window title +#[derive(Component, Reflect, Debug, Clone, PartialEq)] +pub struct WindowTitle(pub String);