diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 9dd91bd86219d..23fff1e5b2c88 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -17,6 +17,7 @@ use bevy_a11y::Focus; mod cursor; mod event; +mod monitor; mod raw_handle; mod system; mod window; @@ -25,6 +26,7 @@ pub use crate::raw_handle::*; pub use cursor::*; pub use event::*; +pub use monitor::*; pub use system::*; pub use window::*; diff --git a/crates/bevy_window/src/monitor.rs b/crates/bevy_window/src/monitor.rs new file mode 100644 index 0000000000000..7f4984c60af89 --- /dev/null +++ b/crates/bevy_window/src/monitor.rs @@ -0,0 +1,63 @@ +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::{IVec2, UVec2}; +use bevy_reflect::Reflect; + +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + +/// Represents a monitor attached to the system, which can be used to create windows. +/// +/// This component is synchronized with `winit` through `bevy_winit`, but is effectively +/// read-only as `winit` does not support changing monitor properties. +#[derive(Component, Debug, Clone, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Component)] +pub struct Monitor { + /// The name of the monitor + pub name: Option, + /// The height of the monitor in physical pixels + pub physical_height: u32, + /// The width of the monitor in physical pixels + pub physical_width: u32, + /// The position of the monitor in physical pixels + pub physical_position: IVec2, + /// The refresh rate of the monitor in millihertz + pub refresh_rate_millihertz: Option, + /// The scale factor of the monitor + pub scale_factor: f64, + /// The video modes that the monitor supports + pub video_modes: Vec, +} + +/// A marker component for the primary monitor +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component)] +pub struct PrimaryMonitor; + +impl Monitor { + /// Returns the physical size of the monitor in pixels + pub fn physical_size(&self) -> UVec2 { + UVec2::new(self.physical_width, self.physical_height) + } +} + +/// Represents a video mode that a monitor supports +#[derive(Debug, Clone, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct VideoMode { + /// The resolution of the video mode + pub physical_size: UVec2, + /// The bit depth of the video mode + pub bit_depth: u16, + /// The refresh rate in millihertz + pub refresh_rate_millihertz: u32, +} diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index fdb8ae5c3d90f..a65b0b01d599a 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -901,6 +901,8 @@ pub enum MonitorSelection { Primary, /// Uses the monitor with the specified index. Index(usize), + /// Uses a given [`Monitor`] entity. + Entity(Entity), } /// Presentation mode for a [`Window`]. diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 57a9de8978adb..822fe5b93cb8f 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -6,26 +6,30 @@ use bevy_ecs::{ removal_detection::RemovedComponents, system::{NonSendMut, Query, SystemParamItem}, }; -use bevy_utils::tracing::{error, info, warn}; +use bevy_utils::tracing::{error, info, trace, warn}; use bevy_window::{ - ClosingWindow, RawHandleWrapper, Window, WindowClosed, WindowClosing, WindowCreated, - WindowMode, WindowResized, + ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, + WindowClosing, WindowCreated, WindowMode, WindowResized, }; use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; use winit::event_loop::ActiveEventLoop; use bevy_ecs::query::With; +use bevy_ecs::system::Res; +use bevy_math::{IVec2, UVec2}; #[cfg(target_arch = "wasm32")] use winit::platform::web::WindowExtWebSys; use crate::state::react_to_resize; +use crate::winit_monitors::WinitMonitors; use crate::{ converters::{ self, convert_enabled_buttons, convert_window_level, convert_window_theme, convert_winit_theme, }, - get_best_videomode, get_fitting_videomode, CreateWindowParams, WinitWindows, + get_best_videomode, get_fitting_videomode, CreateMonitorParams, CreateWindowParams, + WinitWindows, }; /// Creates new windows on the [`winit`] backend for each entity with a newly-added @@ -44,6 +48,7 @@ pub fn create_windows( mut adapters, mut handlers, accessibility_requested, + monitors, ): SystemParamItem>, ) { for (entity, mut window, handle_holder) in &mut created_windows { @@ -64,6 +69,7 @@ pub fn create_windows( &mut adapters, &mut handlers, &accessibility_requested, + &monitors, ); if let Some(theme) = winit_window.theme() { @@ -101,6 +107,68 @@ pub fn create_windows( } } +pub fn create_monitors( + event_loop: &EventLoopWindowTarget, + (mut commands, mut monitors): SystemParamItem, +) { + let primary_monitor = event_loop.primary_monitor(); + let mut seen_monitors = vec![false; monitors.monitors.len()]; + + 'outer: for monitor in event_loop.available_monitors() { + for (idx, (m, _)) in monitors.monitors.iter().enumerate() { + if &monitor == m { + seen_monitors[idx] = true; + continue 'outer; + } + } + + let size = monitor.size(); + let position = monitor.position(); + + let entity = commands + .spawn(Monitor { + name: monitor.name(), + physical_height: size.height, + physical_width: size.width, + physical_position: IVec2::new(position.x, position.y), + refresh_rate_millihertz: monitor.refresh_rate_millihertz(), + scale_factor: monitor.scale_factor(), + video_modes: monitor + .video_modes() + .map(|v| { + let size = v.size(); + VideoMode { + physical_size: UVec2::new(size.width, size.height), + bit_depth: v.bit_depth(), + refresh_rate_millihertz: v.refresh_rate_millihertz(), + } + }) + .collect(), + }) + .id(); + + if primary_monitor.as_ref() == Some(&monitor) { + commands.entity(entity).insert(PrimaryMonitor); + } + + monitors.monitors.push((monitor, entity)); + seen_monitors.push(true); + } + + let mut idx = 0; + monitors.monitors.retain(|(m, entity)| { + if seen_monitors[idx] { + idx += 1; + true + } else { + trace!("Despawning monitor {:?}", m.name()); + commands.entity(*entity).despawn(); + idx += 1; + false + } + }); +} + pub(crate) fn despawn_windows( closing: Query>, mut closed: RemovedComponents, @@ -141,6 +209,7 @@ pub struct CachedWindow { pub(crate) fn changed_windows( mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed>, winit_windows: NonSendMut, + monitors: Res, mut window_resized: EventWriter, ) { for (entity, mut window, mut cache) in &mut changed_windows { @@ -296,7 +365,7 @@ pub(crate) fn changed_windows( if let Some(position) = crate::winit_window_position( &window.position, &window.resolution, - winit_window.available_monitors(), + &monitors, winit_window.primary_monitor(), winit_window.current_monitor(), ) { diff --git a/crates/bevy_winit/src/winit_monitors.rs b/crates/bevy_winit/src/winit_monitors.rs new file mode 100644 index 0000000000000..4790bb25930f5 --- /dev/null +++ b/crates/bevy_winit/src/winit_monitors.rs @@ -0,0 +1,12 @@ +use winit::monitor::MonitorHandle; + +use bevy_ecs::entity::Entity; +use bevy_ecs::system::Resource; + +/// Stores [`winit`] monitors and their corresponding entities +#[derive(Resource, Debug, Default)] +pub struct WinitMonitors { + /// Stores [`winit`] monitors and their corresponding entities + // we can't use a `HashMap` here because `MonitorHandle` doesn't implement `Hash` :( + pub monitors: Vec<(MonitorHandle, Entity)>, +} diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 582c2a1efb0d7..640303f2f1636 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -14,6 +14,7 @@ use winit::{ window::{CursorGrabMode as WinitCursorGrabMode, Fullscreen, Window as WinitWindow, WindowId}, }; +use crate::winit_monitors::WinitMonitors; use crate::{ accessibility::{ prepare_accessibility_for_window, AccessKitAdapters, WinitActionRequestHandlers, @@ -39,6 +40,7 @@ pub struct WinitWindows { impl WinitWindows { /// Creates a `winit` window and associates it with our entity. + #[allow(clippy::too_many_arguments)] pub fn create_window( &mut self, event_loop: &ActiveEventLoop, @@ -79,7 +81,7 @@ impl WinitWindows { if let Some(position) = winit_window_position( &window.position, &window.resolution, - event_loop.available_monitors(), + monitors, event_loop.primary_monitor(), None, ) { @@ -354,7 +356,7 @@ pub(crate) fn attempt_grab(winit_window: &WinitWindow, grab_mode: CursorGrabMode pub fn winit_window_position( position: &WindowPosition, resolution: &WindowResolution, - mut available_monitors: impl Iterator, + monitors: &WinitMonitors, primary_monitor: Option, current_monitor: Option, ) -> Option> { @@ -373,7 +375,16 @@ pub fn winit_window_position( current_monitor } Primary => primary_monitor, - Index(n) => available_monitors.nth(*n), + Index(n) => monitors + .monitors + .get(*n) + .as_ref() + .map(|(monitor, _)| monitor.clone()), + Entity(entity) => monitors + .monitors + .iter() + .find(|(_, e)| *e == *entity) + .map(|(m, _)| m.clone()), }; if let Some(monitor) = maybe_monitor {