use bevy::app::AppExit;
use bevy::ecs::change_detection::{DetectChanges, DetectChangesMut};
use bevy::ecs::entity::Entity;
use bevy::ecs::hierarchy::{ChildOf, Children};
use bevy::ecs::message::{MessageReader, MessageWriter};
use bevy::ecs::query::{Added, Changed, Has, Or, With, Without};
use bevy::ecs::system::{
Commands, Local, NonSend, NonSendMut, Populated, Query, Res, ResMut, Single,
};
use bevy::math::IRect;
use bevy::tasks::AsyncComputeTaskPool;
use bevy::tasks::futures_lite::future;
use bevy::time::Time;
use objc2_foundation::NSPoint;
use std::collections::HashSet;
use std::pin::Pin;
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::time::Duration;
use tracing::{Level, debug, error, info, instrument, trace, warn};
use super::{
ActiveDisplayMarker, BProcess, ExistingMarker, FreshMarker, RepositionMarker, ResizeMarker,
RetryFrontSwitch, SpawnWindowTrigger, Timeout, VerifyWindowPosition,
};
use crate::config::{Config, decorations::BorderRadiusOption};
use crate::ecs::display::FloatingLayer;
use crate::ecs::layout::LayoutStrip;
use crate::ecs::params::{ActiveDisplay, Windows};
use crate::ecs::{
ActiveWorkspaceMarker, Bounds, BruteforceWindows, FlashMessage, FocusedMarker, Initializing,
LowPowerMode, MissionControlActive, Position, ReadDisplayProperties, RestoreWindowState,
Scrolling, SendMessageTrigger, SpawnCommandsExt, Unmanaged, WidthRatio, WindowProperties,
};
use crate::events::Event;
use crate::manager::{
Application, Display, Process, Window, WindowManager, WindowOS, bruteforce_windows,
};
use crate::overlay::{FlashMessageManager, OverlayManager};
use crate::platform::{PlatformCallbacks, WinID};
const ANIAMTE_SNAP_THRESHOLD: f32 = 5.0;
const LOOP_MAX_TIMEOUT_FRAME_ACTIVE_MS: u32 = 16;
const LOOP_MAX_TIMEOUT_LOWPOWER_MS: u32 = 500;
const LOOP_MAX_TIMEOUT_MS: u32 = 50;
const LOOP_TIMEOUT_STEP: u32 = 1;
#[allow(clippy::needless_pass_by_value)]
pub fn gather_displays(window_manager: Res<WindowManager>, mut commands: Commands) {
let Ok(active_display_id) = window_manager.active_display_id() else {
error!("Unable to get active display id!");
return;
};
for (display, workspaces) in window_manager.present_displays() {
let origin = Position(display.bounds().min);
let entity = if display.id() == active_display_id {
commands.spawn((display, ActiveDisplayMarker))
} else {
commands.spawn(display)
}
.id();
commands.trigger(ReadDisplayProperties(entity));
let Ok(active_space) = window_manager.active_display_space(active_display_id) else {
return;
};
for id in workspaces {
let active = id == active_space;
commands.spawn_layout_strip(LayoutStrip::new(id, 0), origin.0, entity, active);
commands.spawn((FloatingLayer::new(id), ChildOf(entity)));
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn add_existing_process(
window_manager: Res<WindowManager>,
processes: Populated<(Entity, &BProcess), With<ExistingMarker>>,
mut commands: Commands,
) {
for (entity, process) in processes {
let Ok(app) = window_manager.new_application(&*process.0) else {
error!("creating aplication from process '{}'", process.name());
return;
};
commands.spawn((app, ExistingMarker, ChildOf(entity)));
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<ExistingMarker>();
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn add_existing_application(
window_manager: Res<WindowManager>,
workspaces: Query<&LayoutStrip>,
fresh_apps: Populated<(&mut Application, Entity), With<ExistingMarker>>,
config: Res<Config>,
mut commands: Commands,
) {
let spaces = workspaces
.into_iter()
.map(LayoutStrip::id)
.collect::<Vec<_>>();
let thread_pool = AsyncComputeTaskPool::get();
for (mut app, entity) in fresh_apps {
let mut offscreen_windows = vec![];
if app.observe().is_ok_and(|result| result)
&& let Ok((found_windows, offscreen)) = window_manager
.find_existing_application_windows(&mut app, &spaces, &config)
.inspect_err(|err| warn!("{err}"))
{
offscreen_windows.extend(offscreen);
commands.trigger(SpawnWindowTrigger(found_windows));
}
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<ExistingMarker>();
}
if !offscreen_windows.is_empty() {
let pid = app.pid();
let bundle_id = app.bundle_id();
let config = config.clone();
let bruteforce_task = thread_pool.spawn(async move {
bruteforce_windows(pid, bundle_id.as_deref(), offscreen_windows, &config)
});
commands.spawn(BruteforceWindows(bruteforce_task));
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn finish_setup(
process_query: Query<Entity, With<ExistingMarker>>,
windows: Windows,
applications: Query<&Application>,
mut bruteforce_tasks: Query<(Entity, &mut BruteforceWindows)>,
mut workspaces: Query<(&mut LayoutStrip, Has<ActiveWorkspaceMarker>, &ChildOf)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
if !process_query.is_empty() {
return;
}
if !bruteforce_tasks.is_empty() {
for (entity, mut job) in &mut bruteforce_tasks {
if let Some(found_windows) = future::block_on(future::poll_once(&mut job.0)) {
commands.trigger(SpawnWindowTrigger(found_windows));
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
}
}
return;
}
info!(
"Initialization: found {:?} windows.",
windows.iter().size_hint()
);
let mut focused_managed_window = false;
for (mut strip, active_strip, _) in &mut workspaces {
debug!("space {}: before refresh {strip:?}", strip.id());
let workspace_windows = window_manager
.windows_in_workspace(strip.id())
.inspect_err(|err| {
warn!("failed to get windows on workspace {}: {err}", strip.id());
})
.ok()
.map(|workspace_windows| {
workspace_windows
.into_iter()
.filter_map(|window_id| windows.find_managed(window_id))
.filter(|(window, entity)| {
if window.is_minimized() {
if let Ok(mut entity_commands) = commands.get_entity(*entity) {
entity_commands.try_insert(Unmanaged::Minimized);
}
false
} else {
true
}
})
.collect::<Vec<_>>()
});
let Some(workspace_windows) = workspace_windows else {
continue;
};
for entity in strip.all_windows() {
if !workspace_windows.iter().any(|(_, e)| *e == entity) {
strip.remove(entity);
}
}
for (_, entity) in workspace_windows {
if !strip.contains(entity) {
strip.append(entity);
}
}
debug!("space {}: after refresh {strip:?}", strip.id());
if active_strip && let Some(entity) = strip.first().ok().and_then(|column| column.top()) {
commands.focus_entity(entity, true);
focused_managed_window = true;
}
}
if !focused_managed_window
&& let Some(focused_window_id) = applications
.iter()
.find(|app| app.is_frontmost())
.and_then(|app| app.focused_window_id().ok())
&& let Some((_, entity)) = windows.find(focused_window_id)
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_insert(FocusedMarker);
}
commands.remove_resource::<Initializing>();
commands.trigger(RestoreWindowState);
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn add_launched_process(
window_manager: Res<WindowManager>,
fresh_processes: Populated<(Entity, &mut BProcess, Has<Children>), With<FreshMarker>>,
config: Res<Config>,
mut commands: Commands,
) {
const APP_OBSERVABLE_TIMEOUT_SEC: u64 = 5;
let mut already_seen = HashSet::new();
for (entity, mut process, children) in fresh_processes {
let process = &mut *process.0;
if !already_seen.insert(process.psn()) {
continue;
}
if config.should_force_manage_process(process) {
debug!(
"Forcing management of launched process '{}' despite unobservable policy.",
process.name()
);
process.force_manage(true);
}
if !process.ready() {
continue;
}
if children {
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<FreshMarker>();
}
continue;
}
let Ok(mut app) = window_manager.new_application(process) else {
error!("creating aplication from process '{}'", process.name());
return;
};
if app.observe().is_ok_and(|good| good) {
let timeout = Timeout::new(
Duration::from_secs(APP_OBSERVABLE_TIMEOUT_SEC),
Some(format!(
"{app} did not become observable in {APP_OBSERVABLE_TIMEOUT_SEC}s.",
)),
&mut commands,
);
commands.spawn((app, FreshMarker, timeout, ChildOf(entity)));
} else {
debug!("failed to register some observers {}", process.name());
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn add_launched_application(
app_query: Populated<(&mut Application, Entity, Has<Children>), With<FreshMarker>>,
windows: Windows,
config: Res<Config>,
mut commands: Commands,
) {
let find_window = |window_id| windows.find(window_id);
for (app, entity, has_children) in app_query {
let mut create_windows = app.window_list(&config);
create_windows.retain(|window| find_window(window.id()).is_none());
if !create_windows.is_empty() {
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<FreshMarker>();
}
debug!(
"spawn! (polling path found {} new windows for {entity})",
create_windows.len(),
);
commands.trigger(SpawnWindowTrigger(create_windows));
} else if has_children {
debug!("removing FreshMarker from {entity}: windows already created via AXCreated");
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<FreshMarker>();
}
}
}
}
#[allow(clippy::type_complexity)]
pub(super) fn fresh_marker_cleanup(
cleanup: Populated<
(Entity, Has<FreshMarker>, &Timeout),
Or<(With<BProcess>, With<Application>)>,
>,
mut commands: Commands,
) {
for (entity, fresh, _) in cleanup {
if !fresh && let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<Timeout>();
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn timeout_ticker(
timers: Populated<(Entity, &mut Timeout)>,
clock: Res<Time>,
mut commands: Commands,
) {
for (entity, mut timeout) in timers {
if timeout.timer.is_finished() {
trace!("Despawning entity {entity} due to timeout.");
if let Some(system_id) = timeout.system_id.take() {
commands.run_system(system_id);
commands.unregister_system(system_id);
}
trace!("Removing timer {entity}");
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
} else {
timeout.timer.tick(clock.delta());
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn retry_front_switch(
retries: Populated<(Entity, &RetryFrontSwitch)>,
applications: Query<&Application>,
mut commands: Commands,
) {
for (entity, retry) in retries.iter() {
let Ok(app) = applications.get(retry.0) else {
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
continue;
};
if !app.is_frontmost() {
debug!("Discarding stale front switch retry (app no longer frontmost).");
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
continue;
}
if let Ok(focused_id) = app.focused_window_id() {
debug!("Front switch retry succeeded for window {focused_id}.");
commands.trigger(SendMessageTrigger(Event::WindowFocused {
window_id: focused_id,
}));
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn animate_entities(
animate: Populated<(&mut Position, Entity, &RepositionMarker)>,
time: Res<Time>,
config: Res<Config>,
mut commands: Commands,
) {
let rate = config.animation_speed();
let t = (1.0 - (-rate * time.delta_secs_f64()).exp()).clamp(0.0, 1.0) as f32;
animate
.into_iter()
.for_each(|(mut position, entity, RepositionMarker(origin))| {
let target = origin.as_vec2();
let current = position.0.as_vec2();
let lerped = current.lerp(target, t);
let finished = (target - lerped).length() <= ANIAMTE_SNAP_THRESHOLD;
let new_pos = if finished {
*origin
} else {
lerped.round().as_ivec2()
};
trace!(
"entity {entity} source {} dest {origin} t {t:.3} moving to {new_pos}",
position.0,
);
position.0 = new_pos;
if finished && let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<RepositionMarker>();
}
});
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn animate_resize_entities(
animate: Populated<(&mut Bounds, Entity, &ResizeMarker)>,
time: Res<Time>,
config: Res<Config>,
mut commands: Commands,
) {
let rate = config.animation_speed();
let t = (1.0 - (-rate * time.delta_secs_f64()).exp()).clamp(0.0, 1.0) as f32;
animate
.into_iter()
.for_each(|(mut bounds, entity, ResizeMarker(size))| {
let target = size.as_vec2();
let current = bounds.0.as_vec2();
let lerped = current.lerp(target, t);
let finished = (target - lerped).length() <= ANIAMTE_SNAP_THRESHOLD;
let new_size = if finished {
*size
} else {
lerped.round().as_ivec2()
};
trace!(
"entity {entity} source {} dest {size} t {t:.3} resizing to {new_size}",
bounds.0,
);
bounds.0 = new_size;
if finished && let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<ResizeMarker>();
}
});
}
#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
pub(super) fn pump_events(
mut exit: MessageWriter<AppExit>,
mut messages: MessageWriter<Event>,
low_power_mode: Option<Res<LowPowerMode>>,
incoming_events: Option<NonSend<Receiver<Event>>>,
platform: Option<NonSendMut<Pin<Box<PlatformCallbacks>>>>,
repositioning: Query<(), With<RepositionMarker>>,
resizing: Query<(), With<ResizeMarker>>,
scrolling: Query<(), With<Scrolling>>,
flash_messages: Query<(), With<FlashMessage>>,
mut timeout: Local<u32>,
) {
let Some((ref mut platform, incoming_events)) = platform.zip(incoming_events) else {
return;
};
platform.pump_cocoa_event_loop(f64::from(*timeout) / 1000.0);
let mut received_events = Vec::new();
let mut pending_mouse = None;
loop {
match incoming_events.recv_timeout(Duration::from_millis(1)) {
Ok(Event::Exit) | Err(RecvTimeoutError::Disconnected) => {
exit.write(AppExit::Success);
break;
}
Ok(event) => {
if matches!(event, Event::MouseMoved { .. }) {
pending_mouse = Some(event);
} else {
received_events.extend(pending_mouse.take());
received_events.push(event);
}
*timeout = LOOP_TIMEOUT_STEP;
}
Err(RecvTimeoutError::Timeout) => {
received_events.extend(pending_mouse.take());
messages.write_batch(received_events);
let frame_active = !repositioning.is_empty()
|| !resizing.is_empty()
|| !scrolling.is_empty()
|| !flash_messages.is_empty();
let low_power = low_power_mode.is_some_and(|low_power| low_power.0);
let timeout_limit = if frame_active {
LOOP_MAX_TIMEOUT_FRAME_ACTIVE_MS
} else if low_power {
LOOP_MAX_TIMEOUT_LOWPOWER_MS
} else {
LOOP_MAX_TIMEOUT_MS
};
*timeout = timeout.min(timeout_limit) + LOOP_TIMEOUT_STEP;
break;
}
}
}
}
#[allow(clippy::needless_pass_by_value, clippy::type_complexity)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn window_resized_update_frame(
mut messages: MessageReader<Event>,
mut windows: Query<
(
&mut Window,
Entity,
&Position,
&mut Bounds,
Option<&Unmanaged>,
),
Without<LayoutStrip>,
>,
mut workspaces: Query<(&LayoutStrip, &mut Position)>,
) {
for event in messages.read() {
let Event::WindowResized { window_id } = event else {
continue;
};
let Some((mut window, entity, position, mut bounds, unmanaged)) = windows
.iter_mut()
.find(|window| window.0.id() == *window_id)
else {
continue;
};
if matches!(unmanaged, Some(Unmanaged::Minimized | Unmanaged::Hidden)) {
continue;
}
let Ok(new_frame) = window.update_frame() else {
continue;
};
let active_strip = workspaces
.iter_mut()
.find(|(strip, _)| strip.contains(entity));
let tabbed = active_strip
.as_ref()
.is_some_and(|strip| strip.0.tabbed(entity));
let old_frame = IRect::from_corners(position.0, position.0 + bounds.0);
if old_frame.size() != new_frame.size() {
if tabbed {
bounds.bypass_change_detection().0 = new_frame.size();
} else {
bounds.0 = new_frame.size();
}
}
let Some((strip, mut strip_position)) = active_strip else {
continue;
};
if tabbed {
continue;
}
if old_frame.min.x != new_frame.min.x {
let shift = (old_frame.size() - new_frame.size()).with_y(0);
strip_position.0.x += shift.x;
}
let diff = old_frame.min.y - new_frame.min.y;
if diff.abs() > 0
&& let Some(above_entity) = strip.above(entity)
&& let Ok((_, _, _, mut above_bounds, _)) = windows.get_mut(above_entity)
&& above_bounds.0.y - diff > 200
{
above_bounds.0.y -= diff;
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn window_moved_update_frame(
mut messages: MessageReader<Event>,
mut windows: Query<
(&mut Window, &mut Position, &Bounds, Option<&Unmanaged>),
Without<LayoutStrip>,
>,
) {
for event in messages.read() {
let Event::WindowMoved { window_id } = event else {
continue;
};
let Some((mut window, mut position, bounds, unmanaged)) = windows
.iter_mut()
.find(|window| window.0.id() == *window_id)
else {
continue;
};
if matches!(unmanaged, Some(Unmanaged::Minimized | Unmanaged::Hidden)) {
continue;
}
let Ok(new_frame) = window.update_frame() else {
continue;
};
let old_frame = IRect::from_corners(position.0, position.0 + bounds.0);
if old_frame.min != new_frame.min {
position.0 = new_frame.min;
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn gather_initial_processes(
receiver: Option<NonSendMut<Receiver<Event>>>,
mut displays: Query<&mut Display>,
mut commands: Commands,
) {
let Some(receiver) = receiver else {
return;
};
let mut initial_processes: Vec<BProcess> = Vec::new();
let mut initial_config = None;
loop {
match receiver.recv().expect("error reading initial processes") {
Event::ProcessesLoaded | Event::Exit => break,
Event::ApplicationLaunched { psn, observer } => {
initial_processes.push(Process::new(&psn, observer.clone()).into());
}
Event::InitialConfig(config) => {
let height = config.menubar_height();
for mut display in &mut displays {
display.set_menubar_height_override(height);
}
initial_config = Some(config);
}
event => warn!("Stray event during initial process gathering: {event:?}"),
}
}
while let Some(mut process) = initial_processes.pop() {
let forced = initial_config
.as_ref()
.is_some_and(|c| c.should_force_manage_process(&**process));
if process.is_observable() || forced {
if forced {
debug!(
"Forcing management of existing process '{}' despite unobservable policy.",
process.name()
);
process.force_manage(true);
} else {
debug!("Adding existing process {}", process.name());
}
commands.spawn((ExistingMarker, process));
} else {
debug!(
"Existing application '{}' is not observable, ignoring it.",
process.name(),
);
}
}
if let Some(config) = initial_config {
commands.insert_resource(config);
}
}
#[derive(Default)]
pub(super) struct OverlayWindowConfigCache {
window_id: Option<WinID>,
focused_border_radius: Option<f64>,
detected_border_radius: Option<f64>,
}
#[allow(clippy::needless_pass_by_value, clippy::type_complexity)]
pub(super) fn update_overlays(
active_workspace: Populated<(Has<Scrolling>, &LayoutStrip), With<ActiveWorkspaceMarker>>,
windows: Windows,
applications: Query<&Application>,
overlay_mgr: Option<NonSendMut<OverlayManager>>,
mission_control_active: Res<MissionControlActive>,
config: Res<Config>,
mut window_config_cache: Local<OverlayWindowConfigCache>,
) {
use crate::overlay::BorderParams;
use objc2_foundation::{NSPoint, NSRect, NSSize};
let Some(mut overlay_mgr) = overlay_mgr else {
return;
};
let dim_opacity = config.dim_inactive_opacity();
let border_enabled = config.border_active_window();
let Some((swiping, active_strip)) = active_workspace.iter().next() else {
return;
};
if swiping || mission_control_active.0 || active_strip.is_fullscreen() {
overlay_mgr.hide_all();
return;
}
if dim_opacity == 0.0 && !border_enabled {
overlay_mgr.remove_all();
return;
}
let (focused_abs_cg, focused_window_id) = if let Some((window, _, unmanaged)) = windows
.focused()
.and_then(|(_, entity)| windows.get_managed(entity))
&& unmanaged.is_none()
&& !window.is_full_screen()
{
let frame = window.frame();
let h_pad = window.horizontal_padding();
let v_pad = window.vertical_padding();
let focused_abs_cg = Some(NSRect::new(
NSPoint::new(
f64::from(frame.min.x + h_pad),
f64::from(frame.min.y + v_pad),
),
NSSize::new(
f64::from(frame.width() - 2 * h_pad),
f64::from(frame.height() - 2 * v_pad),
),
));
(focused_abs_cg, window.id())
} else {
overlay_mgr.hide_all();
return;
};
let border_params = if border_enabled {
if window_config_cache.window_id != Some(focused_window_id) || config.is_changed() {
let Some((window, _, parent)) = windows.find_parent(focused_window_id) else {
return;
};
let Ok(app) = applications.get(parent) else {
return;
};
let properties = WindowProperties::new(app, window, &config);
window_config_cache.window_id = Some(focused_window_id);
window_config_cache.focused_border_radius = properties.border_radius();
window_config_cache.detected_border_radius = window.border_radius();
}
let calculated_radius = match config.border_radius() {
BorderRadiusOption::Auto => window_config_cache.detected_border_radius.unwrap_or(10.0),
BorderRadiusOption::Value(value) => value.max(0.0),
};
Some(BorderParams {
color: config.border_color(),
opacity: config.border_opacity(),
width: config.border_width(),
radius: window_config_cache
.focused_border_radius
.unwrap_or(calculated_radius),
})
} else {
window_config_cache.window_id = None;
None
};
let dim_color = config.dim_inactive_color();
overlay_mgr.update(
dim_opacity,
dim_color,
focused_abs_cg,
border_params.as_ref(),
);
}
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn commit_window_position(
mut moved_windows: Populated<(&mut Window, &Position), Changed<Position>>,
) {
moved_windows
.par_iter_mut()
.for_each(|(mut window, position)| window.reposition(position.0));
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn verify_window_position(
mut windows: Populated<(Entity, &mut Window, &Position, &mut VerifyWindowPosition)>,
mut commands: Commands,
) {
for (entity, mut window, position, mut verification) in &mut windows {
if window
.update_frame()
.is_ok_and(|frame| frame.min == position.0)
{
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<VerifyWindowPosition>();
}
continue;
}
window.reposition(position.0);
if verification.tick()
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_remove::<VerifyWindowPosition>();
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn commit_window_size(
active_display: ActiveDisplay,
mut resized_windows: Populated<(&mut Window, &Bounds, &mut WidthRatio), Changed<Bounds>>,
) {
let display_bounds = active_display.bounds();
resized_windows
.par_iter_mut()
.for_each(|(mut window, size, mut width_ratio)| {
width_ratio.0 = f64::from(size.0.x) / f64::from(display_bounds.width());
window.resize(size.0);
});
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn cleanup_on_exit(
mut exit_events: MessageReader<AppExit>,
mut all_windows: Query<&mut Window>,
displays: Query<&Display>,
window_manager: Res<WindowManager>,
mut overlay_mgr: Option<NonSendMut<OverlayManager>>,
) {
for _ in exit_events.read() {
let ids = all_windows.iter().map(|w| w.id()).collect::<Vec<_>>();
info!("exit cleanup: restoring {} window(s)", ids.len());
window_manager.dim_windows(&ids, 0.0);
if let Some(ref mut overlay_mgr) = overlay_mgr {
overlay_mgr.remove_all();
}
let display_bounds = displays.iter().map(Display::bounds).collect::<Vec<_>>();
if display_bounds.is_empty() {
return;
}
for mut window in &mut all_windows {
let frame = window.frame();
let center = frame.center();
let bounds = display_bounds
.iter()
.find(|b| {
center.x >= b.min.x
&& center.x <= b.max.x
&& center.y >= b.min.y
&& center.y <= b.max.y
})
.copied()
.unwrap_or(display_bounds[0]);
let mut size = frame.size();
if size.x > bounds.width() || size.y > bounds.height() {
let new_size = bevy::math::IVec2::new(
size.x.min(bounds.width() * 9 / 10),
size.y.min(bounds.height() * 9 / 10),
);
window.resize(new_size);
size = new_size;
}
let origin = bevy::math::IVec2::new(
bounds.min.x + (bounds.width() - size.x) / 2,
bounds.min.y + (bounds.height() - size.y) / 2,
);
info!(
"exit cleanup: window {} -> origin {:?}, size {:?}",
window.id(),
origin,
size
);
window.reposition(origin);
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn update_flash_messages(
messages: Populated<(Entity, &FlashMessage, &Timeout)>,
active_display: Single<(&Display, Entity), With<ActiveDisplayMarker>>,
flash_mgr: Option<NonSendMut<FlashMessageManager>>,
mut commands: Commands,
) {
let Some(mut flash_manager) = flash_mgr else {
return;
};
if messages.is_empty() {
flash_manager.remove();
return;
}
let (display, _) = *active_display;
let bounds = display.bounds();
let top_right = NSPoint::new(f64::from(bounds.max.x), f64::from(bounds.min.y));
let mut alive: Option<(Entity, &str, &Timeout)> = None;
let mut stale: Vec<Entity> = Vec::new();
for (entity, FlashMessage(flash), timeout) in messages {
if timeout.timer.is_finished() {
stale.push(entity);
continue;
}
match alive {
None => alive = Some((entity, flash, timeout)),
Some((prev_entity, _, prev_timeout)) => {
if timeout.timer.remaining() > prev_timeout.timer.remaining() {
stale.push(prev_entity);
alive = Some((entity, flash, timeout));
} else {
stale.push(entity);
}
}
}
}
for entity in stale {
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
}
if let Some((_, flash, timeout)) = alive {
let opacity = timeout.timer.fraction_remaining();
flash_manager.show(flash, opacity, top_right);
} else {
flash_manager.remove();
}
}
pub(crate) fn update_low_power_state(low_power_mode: Option<ResMut<LowPowerMode>>) {
let Some(mut state) = low_power_mode else {
return;
};
let process_info = objc2_foundation::NSProcessInfo::processInfo();
state.0 = process_info.isLowPowerModeEnabled();
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn window_creation_event(mut messages: MessageReader<Event>, mut commands: Commands) {
for event in messages.read() {
let Event::WindowCreated { element } = event else {
continue;
};
if let Ok(window) = WindowOS::new(element)
.inspect_err(|err| {
trace!("not adding window {element:?}: {err}");
})
.map(|window| Window::new(Box::new(window)))
{
commands.trigger(SpawnWindowTrigger(vec![window]));
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn detect_tabbed_windows(
created: Populated<(Entity, &Position, &Bounds, &ChildOf), Added<Window>>,
windows: Query<(Entity, &Window, &Position, &Bounds, &ChildOf), With<Window>>,
apps: Query<Entity, With<Application>>,
mut workspaces: Query<(&mut LayoutStrip, Has<ActiveWorkspaceMarker>)>,
window_manager: Res<WindowManager>,
active_display: Single<&Display, With<ActiveDisplayMarker>>,
mut commands: Commands,
) {
let display_bounds = active_display.bounds();
let Some(workspace_entities) = workspaces
.iter()
.find_map(|(strip, active)| active.then_some(strip.all_windows()))
else {
return;
};
for (entity, Position(position), Bounds(bounds), child) in created {
let Ok(app_entity) = apps.get(child.parent()) else {
continue;
};
let mut same_size = workspace_entities
.iter()
.filter_map(|e| windows.get(*e).ok())
.filter(|(leader, _, _, Bounds(leader_bounds), child)| {
*leader != entity
&& child.parent() == app_entity
&& leader_bounds.chebyshev_distance(*bounds) <= 1
})
.collect::<Vec<_>>();
let tabbed = same_size
.iter()
.find_map(|(leader, window, Position(leader_position), _, _)| {
(leader_position.chebyshev_distance(*position) <= 1)
.then_some((*leader, window.id()))
})
.or_else(|| {
same_size.sort_by_key(|(_, _, Position(candidate_position), _, _)| {
position.x.abs_diff(candidate_position.x)
});
same_size.into_iter().find_map(
|(leader, window, Position(leader_position), Bounds(leader_bounds), _)| {
let offscreen = !display_bounds.contains(*leader_position)
|| !display_bounds.contains(*leader_position + leader_bounds);
offscreen.then_some((leader, window.id()))
},
)
});
if let Some((leader, leader_id)) = tabbed
&& window_manager
.windows_on_screen()
.is_some_and(|ids| !ids.contains(&leader_id))
&& let Some((mut strip, _)) =
workspaces.iter_mut().find(|strip| strip.0.contains(leader))
&& strip.contains(leader)
{
debug!("Tabbed window detected: adding {entity} to leader {leader}");
if strip
.convert_to_tabs(leader, entity)
.inspect_err(|err| error!("Failed to convert to tabs: {err}"))
.is_ok()
{
commands.focus_entity(entity, false);
}
}
}
}