#![doc = include_str!("../README.md")]
#[cfg(target_os = "macos")]
mod macos_tabbing_fix;
mod monitors;
mod platform;
mod restore_plan;
mod state;
mod state_format;
mod systems;
mod types;
#[cfg(all(target_os = "windows", feature = "workaround-winit-4341"))]
mod windows_dpi_fix;
#[cfg(all(target_os = "linux", feature = "workaround-winit-4445"))]
mod x11_frame_extents;
use std::path::PathBuf;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
pub use monitors::CurrentMonitor;
pub use monitors::MonitorInfo;
use monitors::MonitorPlugin;
pub use monitors::Monitors;
use monitors::init_monitors;
pub use platform::Platform;
pub use state_format::WindowKey;
pub use types::ManagedWindow;
pub use types::ManagedWindowPersistence;
use types::ManagedWindowRegistry;
use types::RestoreWindowConfig;
use types::SavedWindowMode;
use types::TargetPosition;
pub use types::WindowRestoreMismatch;
pub use types::WindowRestored;
pub struct WindowManagerPlugin;
impl WindowManagerPlugin {
#[must_use]
#[expect(clippy::expect_used, reason = "fail fast if path cannot be determined")]
pub fn with_app_name(app_name: impl Into<String>) -> impl Plugin {
WindowManagerPluginCustomPath {
path: state::get_state_path_for_app(&app_name.into())
.expect("Could not determine state file path"),
persistence: ManagedWindowPersistence::default(),
}
}
#[must_use]
pub fn with_path(path: impl Into<PathBuf>) -> impl Plugin {
WindowManagerPluginCustomPath {
path: path.into(),
persistence: ManagedWindowPersistence::default(),
}
}
#[must_use]
#[expect(clippy::expect_used, reason = "fail fast if path cannot be determined")]
pub fn with_persistence(persistence: ManagedWindowPersistence) -> impl Plugin {
WindowManagerPluginCustomPath {
path: state::get_default_state_path().expect("Could not determine state file path"),
persistence,
}
}
}
impl Plugin for WindowManagerPlugin {
#[expect(clippy::expect_used, reason = "fail fast if path cannot be determined")]
fn build(&self, app: &mut App) {
let path = state::get_default_state_path().expect("Could not determine state file path");
build_plugin(app, path, ManagedWindowPersistence::default());
}
}
struct WindowManagerPluginCustomPath {
path: PathBuf,
persistence: ManagedWindowPersistence,
}
impl Plugin for WindowManagerPluginCustomPath {
fn build(&self, app: &mut App) {
build_plugin(app, self.path.clone(), self.persistence.clone());
}
}
fn hide_window_on_creation(add: On<Add, PrimaryWindow>, mut windows: Query<&mut Window>) {
debug!(
"[hide_window_on_creation] Observer fired for entity {:?}",
add.entity
);
if let Ok(mut window) = windows.get_mut(add.entity) {
debug!("[hide_window_on_creation] Setting window.visible = false");
window.visible = false;
}
}
fn on_managed_window_added(
add: On<Add, ManagedWindow>,
mut managed: Query<&mut ManagedWindow>,
mut registry: ResMut<ManagedWindowRegistry>,
config: Res<RestoreWindowConfig>,
monitors: Res<Monitors>,
windows: Query<&Window>,
primary_query: Query<(), With<PrimaryWindow>>,
) {
let entity = add.entity;
let Ok(mut managed_window) = managed.get_mut(entity) else {
return;
};
let name = managed_window.window_name.clone();
if primary_query.get(entity).is_ok() {
warn!(
"[on_managed_window_added] `ManagedWindow` cannot be added to the primary window (entity {entity:?}). \
The primary window is managed automatically under the key \"{key}\".",
key = state::PRIMARY_WINDOW_KEY,
);
return;
}
let unique_name = if registry.names.contains(&name) {
debug_assert!(false, "Duplicate ManagedWindow name: \"{name}\"");
let mut suffix = 2;
loop {
let candidate = format!("{name}-{suffix}");
if !registry.names.contains(&candidate) {
break candidate;
}
suffix += 1;
}
} else {
name.clone()
};
if unique_name != name {
warn!(
"[on_managed_window_added] Duplicate ManagedWindow name: \"{name}\" — renamed to \"{unique_name}\" for entity {entity:?}"
);
managed_window.window_name.clone_from(&unique_name);
}
registry.names.insert(unique_name.clone());
registry.entities.insert(entity, unique_name.clone());
debug!(
"[on_managed_window_added] Registered managed window \"{unique_name}\" on entity {entity:?}"
);
let existing = state::load_all_states(&config.path);
let already_saved = existing
.as_ref()
.is_some_and(|s| s.contains_key(&WindowKey::Managed(unique_name.clone())));
if !already_saved && let Ok(window) = windows.get(entity) {
let monitor = match window.position {
bevy::window::WindowPosition::At(pos) => {
*monitors.monitor_for_window(pos, window.physical_width(), window.physical_height())
},
_ => *monitors.first(),
};
let logical_position = match window.position {
bevy::window::WindowPosition::At(pos) => {
let lx = (f64::from(pos.x) / monitor.scale).round() as i32;
let ly = (f64::from(pos.y) / monitor.scale).round() as i32;
Some((lx, ly))
},
_ => None,
};
let window_state = types::WindowState {
logical_position,
logical_width: window.width() as u32,
logical_height: window.height() as u32,
monitor_scale: monitor.scale,
monitor_index: monitor.index,
mode: SavedWindowMode::Windowed,
app_name: String::new(),
};
let mut states = existing.unwrap_or_default();
states.insert(WindowKey::Managed(unique_name.clone()), window_state);
state::save_all_states(&config.path, &states);
debug!("[on_managed_window_added] Saved initial state for \"{unique_name}\"");
}
}
#[allow(clippy::type_complexity)]
fn on_managed_window_removed(
remove: On<Remove, ManagedWindow>,
mut registry: ResMut<ManagedWindowRegistry>,
config: Res<RestoreWindowConfig>,
persistence: Res<ManagedWindowPersistence>,
monitors: Res<Monitors>,
all_windows: Query<
(
Entity,
&Window,
Option<&CurrentMonitor>,
Option<&ManagedWindow>,
),
Or<(With<PrimaryWindow>, With<ManagedWindow>)>,
>,
primary_q: Query<(), With<PrimaryWindow>>,
) {
let entity = remove.entity;
if let Some(name) = registry.entities.remove(&entity) {
if *persistence == ManagedWindowPersistence::ActiveOnly {
systems::save_active_window_state(
&config,
&monitors,
&all_windows,
&primary_q,
Some(entity),
);
debug!(
"[on_managed_window_removed] Rebuilt state file without \"{name}\" (ActiveOnly)"
);
}
registry.names.remove(&name);
debug!(
"[on_managed_window_removed] Unregistered managed window \"{name}\" from entity {entity:?}"
);
}
}
#[allow(clippy::type_complexity)]
fn on_persistence_changed(
persistence: Res<ManagedWindowPersistence>,
config: Res<RestoreWindowConfig>,
monitors: Res<Monitors>,
all_windows: Query<
(
Entity,
&Window,
Option<&CurrentMonitor>,
Option<&ManagedWindow>,
),
Or<(With<PrimaryWindow>, With<ManagedWindow>)>,
>,
primary_q: Query<(), With<PrimaryWindow>>,
) {
if *persistence == ManagedWindowPersistence::ActiveOnly {
systems::save_active_window_state(&config, &monitors, &all_windows, &primary_q, None);
debug!("[on_persistence_changed] Rebuilt state file for ActiveOnly mode");
}
}
#[allow(clippy::too_many_arguments)]
fn on_managed_window_load(
add: On<Add, ManagedWindow>,
mut commands: Commands,
managed: Query<&ManagedWindow>,
monitors: Res<Monitors>,
winit_info: Option<Res<types::WinitInfo>>,
config: Res<RestoreWindowConfig>,
mut windows: Query<&mut Window>,
primary_monitor: Query<&CurrentMonitor, With<PrimaryWindow>>,
platform: Res<Platform>,
) {
let entity = add.entity;
let Ok(managed_window) = managed.get(entity) else {
return;
};
let name = &managed_window.window_name;
if let Ok(mut window) = windows.get_mut(entity) {
if platform.should_hide_on_startup() {
window.visible = false;
}
}
let key = WindowKey::Managed((*name).clone());
let Some(saved_state) = config.loaded_states.get(&key).cloned() else {
debug!("[on_managed_window_load] No saved state for \"{name}\", showing window");
if let Ok(mut window) = windows.get_mut(entity) {
window.visible = true;
}
return;
};
debug!(
"[on_managed_window_load] Loaded state for \"{name}\": position={:?} logical_size={}x{} monitor_scale={} monitor={} mode={:?}",
saved_state.logical_position,
saved_state.logical_width,
saved_state.logical_height,
saved_state.monitor_scale,
saved_state.monitor_index,
saved_state.mode
);
let Some(winit_info) = winit_info else {
debug!("[on_managed_window_load] WinitInfo not available, showing window for \"{name}\"");
if let Ok(mut window) = windows.get_mut(entity) {
window.visible = true;
}
return;
};
if monitors.is_empty() {
debug!("[on_managed_window_load] No monitors available, showing window for \"{name}\"");
if let Ok(mut window) = windows.get_mut(entity) {
window.visible = true;
}
return;
}
let primary_scale = primary_monitor.iter().next().map_or(1.0, |cm| cm.scale);
restore_managed_window(
entity,
&saved_state,
&monitors,
&winit_info,
&mut commands,
primary_scale,
*platform,
);
}
fn restore_managed_window(
entity: Entity,
saved_state: &types::WindowState,
monitors: &Monitors,
winit_info: &types::WinitInfo,
commands: &mut Commands,
primary_scale: f64,
platform: Platform,
) {
let (target_info, fallback_position, used_fallback) =
restore_plan::resolve_target_monitor_and_position(
saved_state.monitor_index,
saved_state.logical_position,
monitors,
);
if used_fallback {
warn!(
"[restore_managed_window] Target monitor {} not found, falling back to monitor 0",
saved_state.monitor_index
);
}
let decoration = winit_info.decoration();
let target = restore_plan::compute_target_position(
saved_state,
target_info,
fallback_position,
decoration,
primary_scale,
platform,
);
debug!(
"[restore_managed_window] saved_pos={:?} clamped_pos={:?} target_scale={} logical={}x{} physical={}x{} monitor={} mon_pos=({},{}) mon_size=({},{})",
saved_state.logical_position,
target.position,
target.target_scale,
target.logical_width,
target.logical_height,
target.width,
target.height,
target.target_monitor_index,
target_info.position.x,
target_info.position.y,
target_info.size.x,
target_info.size.y,
);
let is_fullscreen = saved_state.mode.is_fullscreen();
commands.entity(entity).insert(target);
if is_fullscreen || !platform.needs_frame_compensation() {
commands.entity(entity).insert(types::X11FrameCompensated);
}
}
fn has_restoring_windows(q: Query<(), With<TargetPosition>>) -> bool { !q.is_empty() }
fn no_restoring_windows(q: Query<(), With<TargetPosition>>) -> bool { q.is_empty() }
fn build_plugin(app: &mut App, path: PathBuf, persistence: ManagedWindowPersistence) {
let platform = Platform::detect();
app.insert_resource(platform);
let should_hide = platform.should_hide_on_startup();
if should_hide {
let mut query = app
.world_mut()
.query_filtered::<&mut Window, With<PrimaryWindow>>();
if let Some(mut window) = query.iter_mut(app.world_mut()).next() {
debug!("[build_plugin] Window already exists, hiding immediately");
window.visible = false;
} else {
debug!("[build_plugin] Window doesn't exist yet, registering observer");
app.add_observer(hide_window_on_creation);
}
} else {
debug!("[build_plugin] Linux X11: skipping window hide for frame extent compensation");
}
#[cfg(target_os = "macos")]
macos_tabbing_fix::init(app);
#[cfg(all(target_os = "windows", feature = "workaround-winit-4341"))]
windows_dpi_fix::init(app);
app.add_plugins(MonitorPlugin)
.insert_resource(RestoreWindowConfig {
path,
loaded_states: std::collections::HashMap::new(),
})
.insert_resource(persistence)
.init_resource::<ManagedWindowRegistry>()
.add_observer(on_managed_window_added)
.add_observer(on_managed_window_removed)
.add_observer(on_managed_window_load)
.add_systems(PreStartup, {
#[cfg(target_os = "linux")]
{
(
systems::init_winit_info,
systems::load_target_position,
systems::move_to_target_monitor,
)
.chain()
.after(init_monitors)
}
#[cfg(not(target_os = "linux"))]
{
(systems::init_winit_info, systems::load_target_position)
.chain()
.after(init_monitors)
}
});
#[cfg(all(target_os = "linux", feature = "workaround-winit-4445"))]
app.add_systems(
Update,
x11_frame_extents::compensate_target_position
.run_if(has_restoring_windows)
.run_if(|p: Res<Platform>| p.is_x11()),
);
app.add_systems(
Update,
(
systems::restore_windows,
systems::check_restore_settling.after(systems::restore_windows),
)
.run_if(has_restoring_windows),
);
app.add_systems(
Update,
(
systems::update_current_monitor,
systems::save_window_state
.run_if(no_restoring_windows)
.after(systems::update_current_monitor),
on_persistence_changed
.run_if(resource_changed::<ManagedWindowPersistence>)
.run_if(no_restoring_windows)
.after(systems::update_current_monitor),
),
);
}