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 <pascal@technocreatives.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Pascal Hertleif <killercup@gmail.com>
This commit is contained in:
charlotte 2024-08-06 05:54:37 -05:00 committed by GitHub
parent 897625c899
commit 3360b45153
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 426 additions and 65 deletions

View file

@ -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

View file

@ -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::*;

View file

@ -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<String>,
/// 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<u32>,
/// The scale factor of the monitor
pub scale_factor: f64,
/// The video modes that the monitor supports
pub video_modes: Vec<VideoMode>,
}
/// 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,
}

View file

@ -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) .

View file

@ -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<T: Event> Plugin for WinitPlugin<T> {
}
app.init_non_send_resource::<WinitWindows>()
.init_resource::<WinitMonitors>()
.init_resource::<WinitSettings>()
.add_event::<WinitEvent>()
.set_runner(winit_runner::<T>)
@ -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>);

View file

@ -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<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
let mut create_monitor = SystemState::<CreateMonitorParams>::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::<CreateWindowParams<Added<Window>>>::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<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
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<T: Event> ApplicationHandler<T> for WinitAppRunnerState<T> {
&mut adapters,
&mut handlers,
&accessibility_requested,
&monitors,
);
let wrapper = RawHandleWrapper::new(winit_window).unwrap();

View file

@ -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<F: QueryFilter + 'static>(
mut adapters,
mut handlers,
accessibility_requested,
monitors,
): SystemParamItem<CreateWindowParams<F>>,
) {
for (entity, mut window, handle_holder) in &mut created_windows {
@ -68,6 +73,7 @@ pub fn create_windows<F: QueryFilter + 'static>(
&mut adapters,
&mut handlers,
&accessibility_requested,
&monitors,
);
if let Some(theme) = winit_window.theme() {
@ -118,6 +124,69 @@ pub fn create_windows<F: QueryFilter + 'static>(
}
}
/// 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<CreateMonitorParams>,
) {
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<Entity, With<ClosingWindow>>,
@ -178,6 +247,7 @@ pub struct CachedWindow {
pub(crate) fn changed_windows(
mut changed_windows: Query<(Entity, &mut Window, &mut CachedWindow), Changed<Window>>,
winit_windows: NonSendMut<WinitWindows>,
monitors: Res<WinitMonitors>,
mut window_resized: EventWriter<WindowResized>,
) {
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(&current_monitor),
WindowMode::SizedFullscreen => get_fitting_videomode(
&current_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(),
) {

View file

@ -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<MonitorHandle> {
self.monitors.get(n).map(|(monitor, _)| monitor.clone())
}
pub fn find_entity(&self, entity: Entity) -> Option<MonitorHandle> {
self.monitors
.iter()
.find(|(_, e)| *e == entity)
.map(|(monitor, _)| monitor.clone())
}
}

View file

@ -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<WinitWindow> {
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<Item = MonitorHandle>,
monitors: &WinitMonitors,
primary_monitor: Option<MonitorHandle>,
current_monitor: Option<MonitorHandle>,
) -> Option<PhysicalPosition<i32>> {
@ -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<MonitorHandle>,
current_monitor: Option<MonitorHandle>,
monitor_selection: &MonitorSelection,
) -> Option<MonitorHandle> {
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),
}
}

View file

@ -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

View file

@ -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,

View file

@ -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<Monitor>>,
mut monitors_removed: RemovedComponents<Monitor>,
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(|| "<no name>".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(|| "<unknown>".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<ButtonInput<KeyCode>>,
) {
for (window, focus) in focused_windows.iter() {
if !focus.focused {
continue;
}
if input.just_pressed(KeyCode::Escape) {
commands.entity(window).despawn();
}
}
}