use bevy_ecs::hierarchy::ChildOf;
use bevy_ecs::prelude::*;
use tracing::{debug, error, info, warn};
use super::super::taffy::TaffyLayoutResource;
use super::super::{
BoxInset, BoxPosition, BoxSize, BoxStyle, Dimension, LayoutRoot, LengthPercentageAuto, Rect,
};
use crate::ecs::window::transition_diag::{self, MonitorRecord, Stamp, TickStart, WindowDpiRecord};
use crate::ecs::world::FrameCount;
use windows::Win32::UI::WindowsAndMessaging::{
CW_USEDEFAULT, GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN,
SM_YVIRTUALSCREEN,
};
pub fn get_virtual_desktop_bounds() -> (i32, i32, i32, i32) {
unsafe {
let x = GetSystemMetrics(SM_XVIRTUALSCREEN);
let y = GetSystemMetrics(SM_YVIRTUALSCREEN);
let width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
(x, y, width, height)
}
}
fn log_enumerated_monitor(monitor: &crate::ecs::Monitor) {
debug!(
handle = monitor.handle,
bounds_left = monitor.bounds.left,
bounds_top = monitor.bounds.top,
bounds_right = monitor.bounds.right,
bounds_bottom = monitor.bounds.bottom,
work_area = format_args!(
"{},{},{},{}",
monitor.work_area.left,
monitor.work_area.top,
monitor.work_area.right,
monitor.work_area.bottom
),
dpi = monitor.dpi,
is_primary = monitor.is_primary,
"[initialize_layout_root] Creating Monitor entity"
);
}
pub fn initialize_layout_root(world: &mut World) {
let existing = world
.query_filtered::<Entity, With<LayoutRoot>>()
.iter(world)
.next();
if existing.is_some() {
return;
}
info!("[initialize_layout_root] Creating LayoutRoot singleton");
let (vx, vy, vw, vh) = get_virtual_desktop_bounds();
debug!(
x = vx,
y = vy,
width = vw,
height = vh,
"[initialize_layout_root] Virtual desktop bounds"
);
let layout_root = world
.spawn((
LayoutRoot,
BoxStyle {
size: Some(BoxSize {
width: Some(Dimension::Px(vw as f32)),
height: Some(Dimension::Px(vh as f32)),
}),
position: Some(BoxPosition::Absolute),
inset: Some(BoxInset(Rect {
left: LengthPercentageAuto::Px(vx as f32),
top: LengthPercentageAuto::Px(vy as f32),
right: LengthPercentageAuto::Auto,
bottom: LengthPercentageAuto::Auto,
})),
..Default::default()
},
))
.id();
{
let mut taffy_res = world.resource_mut::<TaffyLayoutResource>();
if let Err(e) = taffy_res.create_node(layout_root) {
error!(error = ?e, "[initialize_layout_root] Failed to create Taffy node for LayoutRoot");
return;
}
}
let monitors = crate::ecs::window::monitor::enumerate_monitors();
debug!(
count = monitors.len(),
"[initialize_layout_root] Enumerated monitors"
);
for monitor in monitors {
let (width, height) = monitor.physical_size();
let (left, top) = monitor.top_left();
log_enumerated_monitor(&monitor);
let monitor_entity = world
.spawn((
monitor,
ChildOf(layout_root),
BoxStyle {
size: Some(BoxSize {
width: Some(Dimension::Px(width)),
height: Some(Dimension::Px(height)),
}),
position: Some(BoxPosition::Absolute),
inset: Some(BoxInset(Rect {
left: LengthPercentageAuto::Px(left),
top: LengthPercentageAuto::Px(top),
right: LengthPercentageAuto::Auto,
bottom: LengthPercentageAuto::Auto,
})),
..Default::default()
},
))
.id();
let mut taffy_res = world.resource_mut::<TaffyLayoutResource>();
if let Err(e) = taffy_res.create_node(monitor_entity) {
error!(
error = ?e,
"[initialize_layout_root] Failed to create Taffy node for Monitor"
);
}
}
}
pub fn update_monitor_layout_system(
mut query: Query<(&crate::ecs::Monitor, &mut BoxStyle), Changed<crate::ecs::Monitor>>,
) {
for (monitor, mut box_style) in query.iter_mut() {
let (width, height) = monitor.physical_size();
let (left, top) = monitor.top_left();
debug!(
width = width,
height = height,
left = left,
top = top,
"[update_monitor_layout_system] Updating Monitor layout"
);
box_style.size = Some(BoxSize {
width: Some(Dimension::Px(width)),
height: Some(Dimension::Px(height)),
});
box_style.inset = Some(BoxInset(Rect {
left: LengthPercentageAuto::Px(left),
top: LengthPercentageAuto::Px(top),
right: LengthPercentageAuto::Auto,
bottom: LengthPercentageAuto::Auto,
}));
}
}
#[allow(clippy::too_many_arguments)]
pub fn detect_display_change_system(
mut commands: Commands,
mut app: ResMut<crate::ecs::App>,
layout_root: Query<Entity, With<LayoutRoot>>,
mut existing_monitors: Query<(Entity, &mut crate::ecs::Monitor), With<crate::ecs::Monitor>>,
mut windows: Query<(Entity, &crate::ecs::WindowPos, &mut crate::ecs::DPI)>,
mut taffy_res: ResMut<TaffyLayoutResource>,
frame: Res<FrameCount>,
tick_start: Res<TickStart>,
) {
if !app.display_configuration_changed() {
return;
}
info!("[detect_display_change_system] Display configuration changed, updating monitors");
let Ok(root_entity) = layout_root.single() else {
warn!("[detect_display_change_system] LayoutRoot not found, skipping");
app.reset_display_change();
return;
};
let new_monitors = crate::ecs::window::monitor::enumerate_monitors();
debug!(
count = new_monitors.len(),
"[detect_display_change_system] Found monitors"
);
let windows_redriven = apply_monitor_snapshot(
&mut commands,
root_entity,
&mut existing_monitors,
&mut windows,
&mut taffy_res,
new_monitors,
transition_diag::stamp_from_world(&frame, &tick_start),
);
debug!(
windows_redriven = windows_redriven,
"[detect_display_change_system] Display configuration change applied"
);
app.reset_display_change();
}
pub(crate) fn apply_monitor_snapshot(
commands: &mut Commands,
root_entity: Entity,
existing_monitors: &mut Query<(Entity, &mut crate::ecs::Monitor), With<crate::ecs::Monitor>>,
windows: &mut Query<(Entity, &crate::ecs::WindowPos, &mut crate::ecs::DPI)>,
taffy_res: &mut TaffyLayoutResource,
new_monitors: Vec<crate::ecs::Monitor>,
stamp: Stamp,
) -> usize {
let mut existing_map: std::collections::HashMap<isize, (Entity, crate::ecs::Monitor)> =
existing_monitors
.iter()
.map(|(e, m)| (m.handle, (e, m.clone())))
.collect();
let mut updated_monitors: Vec<crate::ecs::Monitor> = Vec::new();
for new_monitor in new_monitors {
if let Some((entity, existing_monitor)) = existing_map.remove(&new_monitor.handle) {
if existing_monitor.differs_in_value(&new_monitor) {
debug!(
entity = ?entity,
handle = new_monitor.handle,
old_bounds = format_args!(
"{},{},{},{}",
existing_monitor.bounds.left,
existing_monitor.bounds.top,
existing_monitor.bounds.right,
existing_monitor.bounds.bottom
),
new_bounds = format_args!(
"{},{},{},{}",
new_monitor.bounds.left,
new_monitor.bounds.top,
new_monitor.bounds.right,
new_monitor.bounds.bottom
),
old_work_area = format_args!(
"{},{},{},{}",
existing_monitor.work_area.left,
existing_monitor.work_area.top,
existing_monitor.work_area.right,
existing_monitor.work_area.bottom
),
new_work_area = format_args!(
"{},{},{},{}",
new_monitor.work_area.left,
new_monitor.work_area.top,
new_monitor.work_area.right,
new_monitor.work_area.bottom
),
old_dpi = existing_monitor.dpi,
new_dpi = new_monitor.dpi,
old_primary = existing_monitor.is_primary,
new_primary = new_monitor.is_primary,
"[detect_display_change_system] Updating Monitor entity"
);
if let Ok((_, mut monitor)) = existing_monitors.get_mut(entity) {
*monitor = new_monitor.clone();
}
if transition_diag::is_enabled() {
transition_diag::emit_line(&transition_diag::monitor_line(&MonitorRecord {
stamp,
entity,
old_dpi: existing_monitor.dpi,
new_dpi: new_monitor.dpi,
old_work_area: existing_monitor.work_area,
new_work_area: new_monitor.work_area,
}));
}
updated_monitors.push(new_monitor);
}
} else {
debug!(
handle = new_monitor.handle,
"[detect_display_change_system] Adding new Monitor"
);
let (width, height) = new_monitor.physical_size();
let (left, top) = new_monitor.top_left();
let monitor_entity = commands
.spawn((
new_monitor,
ChildOf(root_entity),
BoxStyle {
size: Some(BoxSize {
width: Some(Dimension::Px(width)),
height: Some(Dimension::Px(height)),
}),
position: Some(BoxPosition::Absolute),
inset: Some(BoxInset(Rect {
left: LengthPercentageAuto::Px(left),
top: LengthPercentageAuto::Px(top),
right: LengthPercentageAuto::Auto,
bottom: LengthPercentageAuto::Auto,
})),
..Default::default()
},
))
.id();
if let Err(e) = taffy_res.create_node(monitor_entity) {
error!(
error = ?e,
"[detect_display_change_system] Failed to create Taffy node for new Monitor"
);
}
}
}
for (entity, monitor) in existing_map.values() {
debug!(
entity = ?entity,
handle = monitor.handle,
"[detect_display_change_system] Removing Monitor entity"
);
if let Err(e) = taffy_res.remove_node(*entity) {
error!(
error = ?e,
"[detect_display_change_system] Failed to remove Taffy node"
);
}
commands.entity(*entity).despawn();
}
redrive_window_dpi_for_updated_monitors(windows, &updated_monitors, stamp)
}
pub fn window_center(pos: &crate::ecs::WindowPos) -> Option<(i32, i32)> {
let position = pos.position?;
let size = pos.size?;
if position.x == CW_USEDEFAULT || size.width == CW_USEDEFAULT {
return None;
}
Some((position.x + size.width / 2, position.y + size.height / 2))
}
pub trait MonitorBounds {
fn monitor_bounds(&self) -> windows::Win32::Foundation::RECT;
}
impl MonitorBounds for crate::ecs::Monitor {
fn monitor_bounds(&self) -> windows::Win32::Foundation::RECT {
self.bounds
}
}
pub fn monitor_containing<M: MonitorBounds>(monitors: &[M], center: (i32, i32)) -> Option<&M> {
let (x, y) = center;
monitors.iter().find(|m| {
let bounds = m.monitor_bounds();
x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom
})
}
fn redrive_window_dpi_for_updated_monitors(
windows: &mut Query<(Entity, &crate::ecs::WindowPos, &mut crate::ecs::DPI)>,
updated_monitors: &[crate::ecs::Monitor],
stamp: Stamp,
) -> usize {
if updated_monitors.is_empty() {
return 0;
}
let mut rewritten = 0usize;
for (entity, window_pos, mut dpi) in windows.iter_mut() {
let Some(center) = window_center(window_pos) else {
debug!(
entity = ?entity,
"[detect_display_change_system] Window position/size undetermined, DPI redrive skipped"
);
continue;
};
let Some(monitor) = monitor_containing(updated_monitors, center) else {
continue;
};
let new_dpi = crate::ecs::DPI::from_dpi(monitor.dpi as u16, monitor.dpi as u16);
if *dpi == new_dpi {
continue;
}
let old_dpi = *dpi;
*dpi = new_dpi;
rewritten += 1;
if transition_diag::is_enabled() {
transition_diag::emit_line(&transition_diag::windpi_line(&WindowDpiRecord {
stamp,
entity,
old_dpi: u32::from(old_dpi.dpi_x),
new_dpi: u32::from(new_dpi.dpi_x),
}));
}
debug!(
entity = ?entity,
handle = monitor.handle,
center = format_args!("{},{}", center.0, center.1),
old_dpi_x = old_dpi.dpi_x,
old_dpi_y = old_dpi.dpi_y,
new_dpi_x = new_dpi.dpi_x,
new_dpi_y = new_dpi.dpi_y,
"[detect_display_change_system] Redriving window DPI from updated Monitor (no WM_DPICHANGED required)"
);
}
rewritten
}
#[cfg(test)]
#[path = "monitor_systems_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "monitor_systems_transition_tests.rs"]
mod monitor_systems_transition_tests;