From 3360b451533264bfba882bc560b8eeb9e4207267 Mon Sep 17 00:00:00 2001 From: charlotte Date: Tue, 6 Aug 2024 05:54:37 -0500 Subject: [PATCH] Expose winit's `MonitorHandle` (#13669) # Objective Adds a new `Monitor` component representing a winit `MonitorHandle` that can be used to spawn new windows and check for system monitor information. Closes #12955. ## Solution For every winit event, check available monitors and spawn them into the world as components. ## Testing TODO: - [x] Test plugging in and unplugging monitor during app runtime - [x] Test spawning a window on a second monitor by entity id - [ ] Since this touches winit, test all platforms --- ## Changelog - Adds a new `Monitor` component that can be queried for information about available system monitors. ## Migration Guide - `WindowMode` variants now take a `MonitorSelection`, which can be set to `MonitorSelection::Primary` to mirror the old behavior. --------- Co-authored-by: Pascal Hertleif Co-authored-by: Alice Cecile Co-authored-by: Pascal Hertleif --- Cargo.toml | 11 ++ crates/bevy_window/src/lib.rs | 2 + crates/bevy_window/src/monitor.rs | 69 +++++++++++++ crates/bevy_window/src/window.rs | 14 +-- crates/bevy_winit/src/lib.rs | 9 +- crates/bevy_winit/src/state.rs | 11 +- crates/bevy_winit/src/system.rs | 132 ++++++++++++++++++++---- crates/bevy_winit/src/winit_monitors.rs | 35 +++++++ crates/bevy_winit/src/winit_windows.rs | 103 ++++++++++++------ examples/README.md | 1 + examples/mobile/src/lib.rs | 2 +- examples/window/monitor_info.rs | 102 ++++++++++++++++++ 12 files changed, 426 insertions(+), 65 deletions(-) create mode 100644 crates/bevy_window/src/monitor.rs create mode 100644 crates/bevy_winit/src/winit_monitors.rs create mode 100644 examples/window/monitor_info.rs diff --git a/Cargo.toml b/Cargo.toml index efee865286..78c01b0c63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3355,3 +3355,14 @@ panic = "abort" rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] all-features = true cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] + +[[example]] +name = "monitor_info" +path = "examples/window/monitor_info.rs" +doc-scrape-examples = true + +[package.metadata.example.monitor_info] +name = "Monitor info" +description = "Displays information about available monitors (displays)." +category = "Window" +wasm = false diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 9dd91bd862..23fff1e5b2 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 0000000000..64fafa89c1 --- /dev/null +++ b/crates/bevy_window/src/monitor.rs @@ -0,0 +1,69 @@ +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 an available monitor as reported by the user's operating system, which can be used +/// to query information about the display, such as its size, position, and video modes. +/// +/// Each monitor corresponds to an entity and can be used to position a monitor using +/// [`crate::window::MonitorSelection::Entity`]. +/// +/// # Warning +/// +/// 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 0e1a441b83..f6196f70c3 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -945,6 +945,8 @@ pub enum MonitorSelection { Primary, /// Uses the monitor with the specified index. Index(usize), + /// Uses a given [`crate::monitor::Monitor`] entity. + Entity(Entity), } /// Presentation mode for a [`Window`]. @@ -1092,7 +1094,7 @@ pub enum WindowMode { #[default] Windowed, /// The window should appear fullscreen by being borderless and using the full - /// size of the screen. + /// size of the screen on the given [`MonitorSelection`]. /// /// When setting this, the window's physical size will be modified to match the size /// of the current monitor resolution, and the logical size will follow based @@ -1102,8 +1104,8 @@ pub enum WindowMode { /// the window's logical size may be different from its physical size. /// If you want to avoid that behavior, you can use the [`WindowResolution::set_scale_factor_override`] function /// or the [`WindowResolution::with_scale_factor_override`] builder method to set the scale factor to 1.0. - BorderlessFullscreen, - /// The window should be in "true"/"legacy" Fullscreen mode. + BorderlessFullscreen(MonitorSelection), + /// The window should be in "true"/"legacy" Fullscreen mode on the given [`MonitorSelection`]. /// /// When setting this, the operating system will be requested to use the /// **closest** resolution available for the current monitor to match as @@ -1111,8 +1113,8 @@ pub enum WindowMode { /// After that, the window's physical size will be modified to match /// that monitor resolution, and the logical size will follow based on the /// scale factor, see [`WindowResolution`]. - SizedFullscreen, - /// The window should be in "true"/"legacy" Fullscreen mode. + SizedFullscreen(MonitorSelection), + /// The window should be in "true"/"legacy" Fullscreen mode on the given [`MonitorSelection`]. /// /// When setting this, the operating system will be requested to use the /// **biggest** resolution available for the current monitor. @@ -1124,7 +1126,7 @@ pub enum WindowMode { /// the window's logical size may be different from its physical size. /// If you want to avoid that behavior, you can use the [`WindowResolution::set_scale_factor_override`] function /// or the [`WindowResolution::with_scale_factor_override`] builder method to set the scale factor to 1.0. - Fullscreen, + Fullscreen(MonitorSelection), } /// Specifies where a [`Window`] should appear relative to other overlapping windows (on top or under) . diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 0635e17235..afea7e9db1 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -24,8 +24,8 @@ use bevy_app::{App, Last, Plugin}; use bevy_ecs::prelude::*; #[allow(deprecated)] use bevy_window::{exit_on_all_closed, Window, WindowCreated}; -pub use system::create_windows; use system::{changed_windows, despawn_windows}; +pub use system::{create_monitors, create_windows}; pub use winit::event_loop::EventLoopProxy; pub use winit_config::*; pub use winit_event::*; @@ -33,6 +33,7 @@ pub use winit_windows::*; use crate::accessibility::{AccessKitAdapters, AccessKitPlugin, WinitActionRequestHandlers}; use crate::state::winit_runner; +use crate::winit_monitors::WinitMonitors; pub mod accessibility; mod converters; @@ -40,6 +41,7 @@ mod state; mod system; mod winit_config; pub mod winit_event; +mod winit_monitors; mod winit_windows; /// [`AndroidApp`] provides an interface to query the application state as well as monitor events @@ -113,6 +115,7 @@ impl Plugin for WinitPlugin { } app.init_non_send_resource::() + .init_resource::() .init_resource::() .add_event::() .set_runner(winit_runner::) @@ -181,4 +184,8 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( NonSendMut<'w, AccessKitAdapters>, ResMut<'w, WinitActionRequestHandlers>, Res<'w, AccessibilityRequested>, + Res<'w, WinitMonitors>, ); + +/// The parameters of the [`create_monitors`] system. +pub type CreateMonitorParams<'w, 's> = (Commands<'w, 's>, ResMut<'w, WinitMonitors>); diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index abafadda1f..92fc03f652 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -35,10 +35,10 @@ use bevy_window::{ use bevy_window::{PrimaryWindow, RawHandleWrapper}; use crate::accessibility::AccessKitAdapters; -use crate::system::CachedWindow; +use crate::system::{create_monitors, CachedWindow}; use crate::{ - converters, create_windows, AppSendEvent, CreateWindowParams, EventLoopProxyWrapper, - UpdateMode, WinitEvent, WinitSettings, WinitWindows, + converters, create_windows, AppSendEvent, CreateMonitorParams, CreateWindowParams, + EventLoopProxyWrapper, UpdateMode, WinitEvent, WinitSettings, WinitWindows, }; /// Persistent state that is used to run the [`App`] according to the current @@ -401,10 +401,13 @@ impl ApplicationHandler for WinitAppRunnerState { } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + let mut create_monitor = SystemState::::from_world(self.world_mut()); // create any new windows // (even if app did not update, some may have been created by plugin setup) let mut create_window = SystemState::>>::from_world(self.world_mut()); + create_monitors(event_loop, create_monitor.get_mut(self.world_mut())); + create_monitor.apply(self.world_mut()); create_windows(event_loop, create_window.get_mut(self.world_mut())); create_window.apply(self.world_mut()); @@ -475,6 +478,7 @@ impl ApplicationHandler for WinitAppRunnerState { mut adapters, mut handlers, accessibility_requested, + monitors, ) = create_window.get_mut(self.world_mut()); let winit_window = winit_windows.create_window( @@ -484,6 +488,7 @@ impl ApplicationHandler for WinitAppRunnerState { &mut adapters, &mut handlers, &accessibility_requested, + &monitors, ); let wrapper = RawHandleWrapper::new(winit_window).unwrap(); diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 7a0dd51a9a..d0f39ef3f0 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -8,8 +8,8 @@ use bevy_ecs::{ }; use bevy_utils::tracing::{error, info, warn}; use bevy_window::{ - ClosingWindow, RawHandleWrapper, Window, WindowClosed, WindowClosing, WindowCreated, - WindowMode, WindowResized, WindowWrapper, + ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, + WindowClosing, WindowCreated, WindowMode, WindowResized, WindowWrapper, }; use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; @@ -18,18 +18,22 @@ use winit::event_loop::ActiveEventLoop; use bevy_app::AppExit; use bevy_ecs::prelude::EventReader; use bevy_ecs::query::With; +use bevy_ecs::system::Res; +use bevy_math::{IVec2, UVec2}; #[cfg(target_os = "ios")] use winit::platform::ios::WindowExtIOS; #[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, select_monitor, CreateMonitorParams, + CreateWindowParams, WinitWindows, }; /// Creates new windows on the [`winit`] backend for each entity with a newly-added @@ -48,6 +52,7 @@ pub fn create_windows( mut adapters, mut handlers, accessibility_requested, + monitors, ): SystemParamItem>, ) { for (entity, mut window, handle_holder) in &mut created_windows { @@ -68,6 +73,7 @@ pub fn create_windows( &mut adapters, &mut handlers, &accessibility_requested, + &monitors, ); if let Some(theme) = winit_window.theme() { @@ -118,6 +124,69 @@ pub fn create_windows( } } +/// Synchronize available monitors as reported by [`winit`] with [`Monitor`] entities in the world. +pub fn create_monitors( + event_loop: &ActiveEventLoop, + (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); + } + + seen_monitors.push(true); + monitors.monitors.push((monitor, entity)); + } + + let mut idx = 0; + monitors.monitors.retain(|(_m, entity)| { + if seen_monitors[idx] { + idx += 1; + true + } else { + info!("Monitor removed {:?}", entity); + commands.entity(*entity).despawn(); + idx += 1; + false + } + }); +} + #[allow(clippy::too_many_arguments)] pub(crate) fn despawn_windows( closing: Query>, @@ -178,6 +247,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 { @@ -191,26 +261,44 @@ pub(crate) fn changed_windows( if window.mode != cache.window.mode { let new_mode = match window.mode { - WindowMode::BorderlessFullscreen => { - Some(Some(winit::window::Fullscreen::Borderless(None))) + WindowMode::BorderlessFullscreen(monitor_selection) => { + Some(Some(winit::window::Fullscreen::Borderless(select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + )))) } - mode @ (WindowMode::Fullscreen | WindowMode::SizedFullscreen) => { - if let Some(current_monitor) = winit_window.current_monitor() { - let videomode = match mode { - WindowMode::Fullscreen => get_best_videomode(¤t_monitor), - WindowMode::SizedFullscreen => get_fitting_videomode( - ¤t_monitor, - window.width() as u32, - window.height() as u32, - ), - _ => unreachable!(), - }; + mode @ (WindowMode::Fullscreen(_) | WindowMode::SizedFullscreen(_)) => { + let videomode = match mode { + WindowMode::Fullscreen(monitor_selection) => get_best_videomode( + &select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + ), + WindowMode::SizedFullscreen(monitor_selection) => get_fitting_videomode( + &select_monitor( + &monitors, + winit_window.primary_monitor(), + winit_window.current_monitor(), + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + window.width() as u32, + window.height() as u32, + ), + _ => unreachable!(), + }; - Some(Some(winit::window::Fullscreen::Exclusive(videomode))) - } else { - warn!("Could not determine current monitor, ignoring exclusive fullscreen request for window {:?}", window.title); - None - } + Some(Some(winit::window::Fullscreen::Exclusive(videomode))) } WindowMode::Windowed => Some(None), }; @@ -336,7 +424,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 0000000000..0b8c0073e8 --- /dev/null +++ b/crates/bevy_winit/src/winit_monitors.rs @@ -0,0 +1,35 @@ +use winit::monitor::MonitorHandle; + +use bevy_ecs::entity::Entity; +use bevy_ecs::system::Resource; + +/// Stores [`winit`] monitors and their corresponding entities +/// +/// # Known Issues +/// +/// On some platforms, physically disconnecting a monitor might result in a +/// panic in [`winit`]'s loop. This will lead to a crash in the bevy app. See +/// [13669] for investigations and discussions. +/// +/// [13669]: https://github.com/bevyengine/bevy/pull/13669 +#[derive(Resource, Debug, Default)] +pub struct WinitMonitors { + /// Stores [`winit`] monitors and their corresponding entities + // We can't use a `BtreeMap` here because clippy complains about using `MonitorHandle` as a key + // on some platforms. Using a `Vec` is fine because we don't expect to have a large number of + // monitors and avoids having to audit the code for `MonitorHandle` equality. + pub(crate) monitors: Vec<(MonitorHandle, Entity)>, +} + +impl WinitMonitors { + pub fn nth(&self, n: usize) -> Option { + self.monitors.get(n).map(|(monitor, _)| monitor.clone()) + } + + pub fn find_entity(&self, entity: Entity) -> Option { + self.monitors + .iter() + .find(|(_, e)| *e == entity) + .map(|(monitor, _)| monitor.clone()) + } +} diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 0fc30e3b3e..24d467dfcd 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -4,7 +4,8 @@ use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; use bevy_utils::{tracing::warn, HashMap}; use bevy_window::{ - CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution, WindowWrapper, + CursorGrabMode, MonitorSelection, Window, WindowMode, WindowPosition, WindowResolution, + WindowWrapper, }; use winit::{ @@ -14,6 +15,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 +41,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, @@ -47,6 +50,7 @@ impl WinitWindows { adapters: &mut AccessKitAdapters, handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, + monitors: &WinitMonitors, ) -> &WindowWrapper { let mut winit_window_attributes = WinitWindow::default_attributes(); @@ -55,31 +59,49 @@ impl WinitWindows { winit_window_attributes = winit_window_attributes.with_visible(false); winit_window_attributes = match window.mode { - WindowMode::BorderlessFullscreen => winit_window_attributes - .with_fullscreen(Some(Fullscreen::Borderless(event_loop.primary_monitor()))), - mode @ (WindowMode::Fullscreen | WindowMode::SizedFullscreen) => { - if let Some(primary_monitor) = event_loop.primary_monitor() { - let videomode = match mode { - WindowMode::Fullscreen => get_best_videomode(&primary_monitor), - WindowMode::SizedFullscreen => get_fitting_videomode( - &primary_monitor, - window.width() as u32, - window.height() as u32, - ), - _ => unreachable!(), - }; + WindowMode::BorderlessFullscreen(monitor_selection) => winit_window_attributes + .with_fullscreen(Some(Fullscreen::Borderless(select_monitor( + monitors, + event_loop.primary_monitor(), + None, + &monitor_selection, + )))), + mode @ (WindowMode::Fullscreen(_) | WindowMode::SizedFullscreen(_)) => { + let videomode = match mode { + WindowMode::Fullscreen(monitor_selection) => get_best_videomode( + &select_monitor( + monitors, + event_loop.primary_monitor(), + None, + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + ), + WindowMode::SizedFullscreen(monitor_selection) => get_fitting_videomode( + &select_monitor( + monitors, + event_loop.primary_monitor(), + None, + &monitor_selection, + ) + .unwrap_or_else(|| { + panic!("Could not find monitor for {:?}", monitor_selection) + }), + window.width() as u32, + window.height() as u32, + ), + _ => unreachable!(), + }; - winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode))) - } else { - warn!("Could not determine primary monitor, ignoring exclusive fullscreen request for window {:?}", window.title); - winit_window_attributes - } + winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode))) } WindowMode::Windowed => { if let Some(position) = winit_window_position( &window.position, &window.resolution, - event_loop.available_monitors(), + monitors, event_loop.primary_monitor(), None, ) { @@ -354,7 +376,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> { @@ -364,17 +386,12 @@ pub fn winit_window_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), - }; + let maybe_monitor = select_monitor( + monitors, + primary_monitor, + current_monitor, + monitor_selection, + ); if let Some(monitor) = maybe_monitor { let screen_size = monitor.size(); @@ -410,3 +427,25 @@ pub fn winit_window_position( } } } + +/// Selects a monitor based on the given [`MonitorSelection`]. +pub fn select_monitor( + monitors: &WinitMonitors, + primary_monitor: Option, + current_monitor: Option, + monitor_selection: &MonitorSelection, +) -> Option { + use bevy_window::MonitorSelection::*; + + 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) => monitors.nth(*n), + Entity(entity) => monitors.find_entity(*entity), + } +} diff --git a/examples/README.md b/examples/README.md index 1203c5ef57..dc58b56cf0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -489,6 +489,7 @@ Example | Description [Clear Color](../examples/window/clear_color.rs) | Creates a solid color window [Custom User Event](../examples/window/custom_user_event.rs) | Handles custom user events within the event loop [Low Power](../examples/window/low_power.rs) | Demonstrates settings to reduce power use for bevy applications +[Monitor info](../examples/window/monitor_info.rs) | Displays information about available monitors (displays). [Multiple Windows](../examples/window/multiple_windows.rs) | Demonstrates creating multiple windows, and rendering to them [Scale Factor Override](../examples/window/scale_factor_override.rs) | Illustrates how to customize the default window settings [Screenshot](../examples/window/screenshot.rs) | Shows how to save screenshots to disk diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index 6665f38a5f..54f561871f 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -14,7 +14,7 @@ fn main() { app.add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { resizable: false, - mode: WindowMode::BorderlessFullscreen, + mode: WindowMode::BorderlessFullscreen(MonitorSelection::Primary), // on iOS, gestures must be enabled. // This doesn't work on Android recognize_rotation_gesture: true, diff --git a/examples/window/monitor_info.rs b/examples/window/monitor_info.rs new file mode 100644 index 0000000000..43b17645f1 --- /dev/null +++ b/examples/window/monitor_info.rs @@ -0,0 +1,102 @@ +//! Displays information about available monitors (displays). + +use bevy::render::camera::RenderTarget; +use bevy::window::{ExitCondition, WindowMode, WindowRef}; +use bevy::{prelude::*, window::Monitor}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins.set(WindowPlugin { + primary_window: None, + exit_condition: ExitCondition::DontExit, + ..default() + })) + .add_systems(Update, (update, close_on_esc)) + .run(); +} + +#[derive(Component)] +struct MonitorRef(Entity); + +fn update( + mut commands: Commands, + monitors_added: Query<(Entity, &Monitor), Added>, + mut monitors_removed: RemovedComponents, + monitor_refs: Query<(Entity, &MonitorRef)>, +) { + for (entity, monitor) in monitors_added.iter() { + // Spawn a new window on each monitor + let name = monitor.name.clone().unwrap_or_else(|| "".into()); + let size = format!("{}x{}px", monitor.physical_height, monitor.physical_width); + let hz = monitor + .refresh_rate_millihertz + .map(|x| format!("{}Hz", x as f32 / 1000.0)) + .unwrap_or_else(|| "".into()); + let position = format!( + "x={} y={}", + monitor.physical_position.x, monitor.physical_position.y + ); + let scale = format!("{:.2}", monitor.scale_factor); + + let window = commands + .spawn(( + Window { + title: name.clone(), + mode: WindowMode::Fullscreen(MonitorSelection::Entity(entity)), + position: WindowPosition::Centered(MonitorSelection::Entity(entity)), + ..default() + }, + MonitorRef(entity), + )) + .id(); + + let camera = commands + .spawn(Camera2dBundle { + camera: Camera { + target: RenderTarget::Window(WindowRef::Entity(window)), + ..default() + }, + ..default() + }) + .id(); + + let info_text = format!( + "Monitor: {name}\nSize: {size}\nRefresh rate: {hz}\nPosition: {position}\nScale: {scale}\n\n", + ); + commands.spawn(( + TextBundle::from_section(info_text, default()).with_style(Style { + position_type: PositionType::Relative, + height: Val::Percent(100.0), + width: Val::Percent(100.0), + ..default() + }), + TargetCamera(camera), + MonitorRef(entity), + )); + } + + // Remove windows for removed monitors + for monitor_entity in monitors_removed.read() { + for (ref_entity, monitor_ref) in monitor_refs.iter() { + if monitor_ref.0 == monitor_entity { + commands.entity(ref_entity).despawn_recursive(); + } + } + } +} + +fn close_on_esc( + mut commands: Commands, + focused_windows: Query<(Entity, &Window)>, + input: Res>, +) { + for (window, focus) in focused_windows.iter() { + if !focus.focused { + continue; + } + + if input.just_pressed(KeyCode::Escape) { + commands.entity(window).despawn(); + } + } +}