use bevy::prelude::*;
use bevy::window::Monitor;
use bevy::window::WindowMode;
use bevy_diagnostic::FrameCount;
use bevy_kana::ToI32;
pub(crate) struct MonitorPlugin;
impl Plugin for MonitorPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreStartup, init_monitors)
.add_systems(Update, update_monitors);
}
}
#[derive(Clone, Copy, Debug, Reflect)]
pub struct MonitorInfo {
pub index: usize,
pub scale: f64,
pub position: IVec2,
pub size: UVec2,
}
#[derive(Resource, Reflect)]
#[reflect(Resource)]
pub struct Monitors {
pub list: Vec<MonitorInfo>,
}
#[derive(Component, Clone, Copy, Debug, Reflect)]
#[reflect(Component)]
pub struct CurrentMonitor {
pub monitor: MonitorInfo,
pub effective_mode: WindowMode,
}
impl std::ops::Deref for CurrentMonitor {
type Target = MonitorInfo;
fn deref(&self) -> &Self::Target { &self.monitor }
}
impl Monitors {
#[must_use]
pub fn at(&self, x: i32, y: i32) -> Option<&MonitorInfo> {
self.list.iter().find(|mon| {
x >= mon.position.x
&& x < mon.position.x + mon.size.x.to_i32()
&& y >= mon.position.y
&& y < mon.position.y + mon.size.y.to_i32()
})
}
#[must_use]
pub fn by_index(&self, index: usize) -> Option<&MonitorInfo> { self.list.get(index) }
#[must_use]
pub const fn is_empty(&self) -> bool { self.list.is_empty() }
#[must_use]
#[expect(
clippy::expect_used,
reason = "fail fast - no monitors means unrecoverable state"
)]
pub fn first(&self) -> &MonitorInfo {
self.list
.first()
.expect("Monitors::first() requires at least one monitor")
}
#[must_use]
pub fn monitor_for_window(&self, position: IVec2, width: u32, height: u32) -> &MonitorInfo {
let center_x = position.x + (width / 2).to_i32();
let center_y = position.y + (height / 2).to_i32();
self.closest_to(center_x, center_y)
}
#[must_use]
#[expect(
clippy::expect_used,
reason = "fail fast - no monitors means unrecoverable state"
)]
pub fn closest_to(&self, x: i32, y: i32) -> &MonitorInfo {
if let Some(monitor) = self.at(x, y) {
return monitor;
}
self.list
.iter()
.min_by_key(|mon| {
let right = mon.position.x + mon.size.x.to_i32();
let bottom = mon.position.y + mon.size.y.to_i32();
let dx = if x < mon.position.x {
mon.position.x - x
} else if x >= right {
x - right + 1
} else {
0
};
let dy = if y < mon.position.y {
mon.position.y - y
} else if y >= bottom {
y - bottom + 1
} else {
0
};
dx * dx + dy * dy
})
.expect("Monitors::closest_to() requires at least one monitor")
}
}
fn build_monitors(monitors: &Query<&Monitor>) -> Monitors {
let list: Vec<_> = monitors
.iter()
.enumerate()
.map(|(idx, mon)| MonitorInfo {
index: idx,
scale: mon.scale_factor,
position: mon.physical_position,
size: mon.physical_size(),
})
.collect();
Monitors { list }
}
pub(crate) fn init_monitors(mut commands: Commands, monitors: Query<&Monitor>) {
let monitors_resource = build_monitors(&monitors);
debug!(
"[init_monitors] Found {} monitors",
monitors_resource.list.len()
);
for mon in &monitors_resource.list {
debug!(
"[init_monitors] Monitor {}: pos=({}, {}) size={}x{} scale={}",
mon.index, mon.position.x, mon.position.y, mon.size.x, mon.size.y, mon.scale
);
}
commands.insert_resource(monitors_resource);
}
fn update_monitors(
mut commands: Commands,
monitors: Query<&Monitor>,
added: Query<Entity, Added<Monitor>>,
mut removed: RemovedComponents<Monitor>,
frame_count: Res<FrameCount>,
current_monitor_q: Query<Option<&CurrentMonitor>, With<bevy::window::PrimaryWindow>>,
) {
let has_changes = !added.is_empty() || removed.read().next().is_some();
if has_changes {
let monitors_resource = build_monitors(&monitors);
let current = current_monitor_q.iter().next().flatten();
if let Some(cm) = current {
debug!(
"[update_monitors] frame={} Monitors changed, now {} monitors, current_monitor_index={} current_monitor_scale={}",
frame_count.0,
monitors_resource.list.len(),
cm.monitor.index,
cm.monitor.scale,
);
} else {
debug!(
"[update_monitors] frame={} Monitors changed, now {} monitors, current_monitor=None",
frame_count.0,
monitors_resource.list.len(),
);
}
commands.insert_resource(monitors_resource);
}
}