bevy_window_manager 0.20.2

Bevy plugin for primary window restoration and multi-monitor support
Documentation
//! Monitor management for window restoration.
//!
//! Provides a `Monitors` resource that maintains a sorted list of monitors,
//! automatically updated when monitors are added or removed.

use bevy::prelude::*;
use bevy::window::Monitor;
use bevy::window::WindowMode;
use bevy_diagnostic::FrameCount;
use bevy_kana::ToI32;

/// Plugin that manages the `Monitors` resource.
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);
    }
}

/// Information about a single monitor.
#[derive(Clone, Copy, Debug, Reflect)]
pub struct MonitorInfo {
    /// Index in the sorted monitor list.
    pub index:    usize,
    /// Scale factor (typically 1.0 or 2.0 on macOS).
    pub scale:    f64,
    /// Physical position of top-left corner.
    pub position: IVec2,
    /// Physical size in pixels.
    pub size:     UVec2,
}

/// Sorted monitor list, updated when monitors change.
///
/// Monitors are sorted with primary (at 0,0) first, then by position.
#[derive(Resource, Reflect)]
#[reflect(Resource)]
pub struct Monitors {
    pub list: Vec<MonitorInfo>,
}

/// Component storing the current monitor and effective window mode.
///
/// This is the single source of truth for which monitor a window is on and its
/// effective display mode. Updated automatically by the plugin's unified monitor
/// detection system.
///
/// The `effective_mode` field reflects what the user actually sees, even when
/// `window.mode` is stale (e.g., macOS green button fullscreen reports `Windowed`).
///
/// Derefs to [`MonitorInfo`] for convenient access to monitor fields:
/// ```ignore
/// fn my_system(q: Query<(&Window, &CurrentMonitor), With<PrimaryWindow>>) {
///     let (window, monitor) = q.single();
///     println!("Monitor {} at scale {}, mode: {:?}", monitor.index, monitor.scale, monitor.effective_mode);
/// }
/// ```
#[derive(Component, Clone, Copy, Debug, Reflect)]
#[reflect(Component)]
pub struct CurrentMonitor {
    /// The monitor this window is currently on.
    pub monitor:        MonitorInfo,
    /// The effective window mode, accounting for OS-level fullscreen changes.
    pub effective_mode: WindowMode,
}

impl std::ops::Deref for CurrentMonitor {
    type Target = MonitorInfo;

    fn deref(&self) -> &Self::Target { &self.monitor }
}

impl Monitors {
    /// Find monitor containing position (x, y).
    #[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()
        })
    }

    /// Get monitor by index in sorted list.
    #[must_use]
    pub fn by_index(&self, index: usize) -> Option<&MonitorInfo> { self.list.get(index) }

    /// Returns true if no monitors are available.
    ///
    /// This can happen when the laptop lid is closed or all displays are disconnected.
    #[must_use]
    pub const fn is_empty(&self) -> bool { self.list.is_empty() }

    /// Get the first monitor (index 0). Used as fallback when no specific monitor is known.
    ///
    /// # Panics
    ///
    /// Panics if no monitors exist (should never happen on a real system).
    #[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")
    }

    /// Find the monitor a window is on, using window center for detection.
    ///
    /// Uses the center point to correctly handle windows spanning monitor boundaries
    /// and to avoid Windows invisible border offset (winit #4107).
    #[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)
    }

    /// Find the monitor at position, or the closest one if outside all bounds.
    ///
    /// Unlike [`at`](Self::at), this always returns a monitor by finding
    /// the closest monitor when position is outside all bounds.
    ///
    /// # Panics
    ///
    /// Panics if no monitors exist (should never happen on a real system).
    #[must_use]
    #[expect(
        clippy::expect_used,
        reason = "fail fast - no monitors means unrecoverable state"
    )]
    pub fn closest_to(&self, x: i32, y: i32) -> &MonitorInfo {
        // Try exact match first
        if let Some(monitor) = self.at(x, y) {
            return monitor;
        }

        // Find closest monitor by distance to bounding box
        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")
    }
}

/// Build monitor list from query (preserves winit enumeration order).
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 }
}

/// Initialize `Monitors` resource at startup.
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);
}

/// Update `Monitors` resource when monitors are added or removed.
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);
    }
}