use bevy::app::AppExit;
use bevy::ecs::entity::Entity;
use bevy::ecs::hierarchy::{ChildOf, Children};
use bevy::ecs::message::{MessageReader, MessageWriter};
use bevy::ecs::query::{Changed, Has, Or, With};
use bevy::ecs::system::{
Commands, Local, NonSend, NonSendMut, ParallelCommands, Populated, Query, Res, Single,
};
use bevy::math::IRect;
use bevy::tasks::AsyncComputeTaskPool;
use bevy::tasks::futures_lite::future;
use bevy::time::Time;
use objc2_core_graphics::CGDirectDisplayID;
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, FocusedMarker, FreshMarker,
PollForNotifications, RepositionMarker, ResizeMarker, RetryFrontSwitch, SpawnWindowTrigger,
Timeout, WMEventTrigger,
};
use crate::config::{Config, decorations::BorderRadiusOption};
use crate::ecs::layout::LayoutStrip;
use crate::ecs::params::{ActiveDisplay, Configuration, Windows};
use crate::ecs::{
ActiveWorkspaceMarker, Bounds, BruteforceWindows, FlashMessage, Initializing,
LocateDockTrigger, Position, RefreshWindowSizes, RestoreWindowState, Scrolling,
SelectedVirtualMarker, StackAdjustedResize, Unmanaged, WidthRatio, WindowDraggedMarker,
WindowProperties, focus_entity, reshuffle_around,
};
use crate::events::Event;
use crate::manager::{
Application, Display, Process, Window, WindowManager, WindowOS, bruteforce_windows,
};
use crate::overlay::{FlashMessageManager, OverlayManager};
use crate::platform::{PlatformCallbacks, WorkspaceId};
const ORPHANED_SPACES_TIMEOUT_SEC: u64 = 30;
#[allow(clippy::needless_pass_by_value)]
pub(super) fn dispatch_toplevel_triggers(
mut messages: MessageReader<Event>,
broken_notifications: Option<Res<PollForNotifications>>,
mut commands: Commands,
) {
for event in messages.read() {
match event {
Event::WindowCreated { element } => {
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]));
}
}
Event::SpaceChanged => {
if broken_notifications.is_some() {
info!(
"Workspace and display notifications arriving correctly. Disabling the polling.",
);
commands.remove_resource::<PollForNotifications>();
}
commands.trigger(WMEventTrigger(event.clone()));
}
Event::WindowTitleChanged { window_id } => {
trace!("WindowTitleChanged: {window_id:?}");
}
Event::MenuClosed { window_id } => {
trace!("MenuClosed event: {window_id:?}");
}
Event::DisplayResized { display_id } => {
debug!("Display Resized: {display_id:?}");
}
Event::DisplayConfigured { display_id } => {
debug!("Display Configured: {display_id:?}");
}
Event::SystemWoke { msg } => {
debug!("system woke: {msg:?}");
}
_ => commands.trigger(WMEventTrigger(event.clone())),
}
}
}
#[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(LocateDockTrigger(entity));
let Ok(active_space) = window_manager.active_display_space(active_display_id) else {
return;
};
for id in workspaces {
let strip = LayoutStrip::new(id, 0);
if id == active_space {
commands.spawn((
strip,
origin.clone(),
ActiveWorkspaceMarker,
SelectedVirtualMarker,
ChildOf(entity),
));
} else {
commands.spawn((
strip,
origin.clone(),
SelectedVirtualMarker,
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)));
commands.entity(entity).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>>,
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)
.inspect_err(|err| warn!("{err}"))
{
offscreen_windows.extend(offscreen);
commands.trigger(SpawnWindowTrigger(found_windows));
}
commands.entity(entity).try_remove::<ExistingMarker>();
if !offscreen_windows.is_empty() {
let pid = app.pid();
let bruteforce_task =
thread_pool.spawn(async move { bruteforce_windows(pid, offscreen_windows) });
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,
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));
commands.entity(entity).despawn();
}
}
return;
}
info!(
"Initialization: found {:?} windows.",
windows.iter().size_hint()
);
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() {
commands.entity(*entity).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()) {
focus_entity(entity, true, &mut commands);
}
}
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>>,
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 !process.ready() {
continue;
}
if children {
commands.entity(entity).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.",
)),
);
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,
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();
create_windows.retain(|window| find_window(window.id()).is_none());
if !create_windows.is_empty() {
commands.entity(entity).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");
commands.entity(entity).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 {
commands.entity(entity).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(message) = &timeout.message {
debug!("{message}");
}
trace!("Removing timer {entity}");
commands.entity(entity).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(WMEventTrigger(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)]
pub(super) fn display_changes_watcher(
displays: Query<(&Display, Has<ActiveDisplayMarker>)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
let Ok(current_display_id) = window_manager.active_display_id() else {
return;
};
let found = displays
.iter()
.find(|(display, _)| display.id() == current_display_id);
if let Some((_, active)) = found {
if active {
return;
}
debug!("detected dislay change from {current_display_id}.");
commands.trigger(WMEventTrigger(Event::DisplayChanged));
} else {
debug!("new display {current_display_id} detected.");
commands.trigger(WMEventTrigger(Event::DisplayAdded {
display_id: current_display_id,
}));
}
let present_displays = window_manager.present_displays();
displays.iter().for_each(|(display, _)| {
if !present_displays
.iter()
.any(|(present_display, _)| present_display.id() == display.id())
{
let display_id = display.id();
debug!("detected removal of display {display_id}");
commands.trigger(WMEventTrigger(Event::DisplayRemoved {
display_id: display.id(),
}));
}
});
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn animate_entities(
mut animate: Populated<(&mut Position, Entity, &RepositionMarker)>,
active_display: ActiveDisplay,
time: Res<Time>,
config: Res<Config>,
commands: ParallelCommands,
) {
let move_ratio = config.animation_speed() * time.delta_secs_f64();
let move_delta = move_ratio * f64::from(active_display.display().width());
animate
.par_iter_mut()
.for_each(|(mut position, entity, RepositionMarker(origin))| {
let delta = position
.0
.as_vec2()
.move_towards(origin.as_vec2(), move_delta as f32)
.as_ivec2();
trace!(
"entity {entity} source {} dest {origin} delta {move_delta} moving to {delta}",
position.0,
);
position.0 = delta;
if *origin == delta {
commands.command_scope(|mut command| {
command.entity(entity).try_remove::<RepositionMarker>();
});
}
});
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn animate_resize_entities(
mut animate: Populated<(&mut Bounds, Entity, &ResizeMarker, Has<RepositionMarker>)>,
active_display: ActiveDisplay,
time: Res<Time>,
config: Res<Config>,
commands: ParallelCommands,
) {
let move_ratio = config.animation_speed() * time.delta_secs_f64();
let move_delta = move_ratio * f64::from(active_display.display().width());
animate
.par_iter_mut()
.for_each(|(mut bounds, entity, ResizeMarker(size), moving)| {
if moving {
let current_size = bounds.0;
if size.x > current_size.x || size.y > current_size.y {
return;
}
}
let delta = bounds
.0
.as_vec2()
.move_towards(size.as_vec2(), move_delta as f32)
.as_ivec2();
trace!(
"entity {entity} source {} dest {size} delta {move_delta} resizing to {delta}",
bounds.0,
);
bounds.0 = delta;
if *size == delta {
commands.command_scope(|mut command| {
command.entity(entity).try_remove::<ResizeMarker>();
});
}
});
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn pump_events(
mut exit: MessageWriter<AppExit>,
mut messages: MessageWriter<Event>,
incoming_events: Option<NonSend<Receiver<Event>>>,
platform: Option<NonSendMut<Pin<Box<PlatformCallbacks>>>>,
mut timeout: Local<u32>,
) {
const LOOP_MAX_TIMEOUT_MS: u32 = 500;
const LOOP_TIMEOUT_STEP: u32 = 1;
let Some((ref mut platform, incoming_events)) = platform.zip(incoming_events) else {
return;
};
platform.pump_cocoa_event_loop(f64::from(*timeout) / 1000.0);
loop {
match incoming_events.recv_timeout(Duration::from_millis(1)) {
Ok(Event::Exit) | Err(RecvTimeoutError::Disconnected) => {
exit.write(AppExit::Success);
break;
}
Ok(event) => {
messages.write(event);
*timeout = LOOP_TIMEOUT_STEP;
}
Err(RecvTimeoutError::Timeout) => {
*timeout = timeout.min(LOOP_MAX_TIMEOUT_MS) + LOOP_TIMEOUT_STEP;
break;
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn window_update_frame(
mut messages: MessageReader<Event>,
mut windows: Query<(
&mut Window,
Entity,
&mut Position,
&mut Bounds,
Has<StackAdjustedResize>,
)>,
focused: Option<Single<Entity, With<FocusedMarker>>>,
active_display: ActiveDisplay,
active_workspace: Query<&Scrolling, With<ActiveWorkspaceMarker>>,
config: Configuration,
mut commands: Commands,
) {
for event in messages.read() {
match event {
Event::WindowMoved { .. } | Event::WindowResized { .. }
if active_workspace
.iter()
.next()
.is_some_and(|marker| marker.is_user_swiping) => {}
Event::WindowMoved { window_id } | Event::WindowResized { window_id } => {
let (entity, old_frame, new_frame) = {
let Some((mut window, entity, mut position, mut bounds, stack_adjusted)) =
windows
.iter_mut()
.find(|window| window.0.id() == *window_id)
else {
continue;
};
let Ok(new_frame) = window.update_frame() else {
continue;
};
if stack_adjusted {
commands.entity(entity).try_remove::<StackAdjustedResize>();
continue;
}
if !active_display.active_strip().contains(entity) {
continue;
}
let old_frame = IRect::from_corners(position.0, position.0 + bounds.0);
if matches!(event, Event::WindowMoved { window_id: _ })
|| old_frame.min != new_frame.min
{
position.0 = new_frame.min;
}
if matches!(event, Event::WindowResized { window_id: _ })
&& bounds.0 != new_frame.size()
{
bounds.0 = new_frame.size();
}
(entity, old_frame, new_frame)
};
if matches!(event, Event::WindowResized { window_id: _ }) && !config.initializing()
{
let is_top_edge_drag = old_frame.min.y != new_frame.min.y
&& old_frame.max.y.abs_diff(new_frame.max.y) <= 2;
if is_top_edge_drag
&& let Some(above_entity) = active_display.active_strip().above(entity)
{
if let Ok((_, _, above_pos, mut bounds, _)) = windows.get_mut(above_entity)
{
let new_height = new_frame.min.y - above_pos.0.y;
if new_height > 0 {
bounds.0.y = new_height;
}
}
commands
.entity(above_entity)
.try_insert(StackAdjustedResize);
}
if let Some(focused) = &focused {
reshuffle_around(**focused, &mut commands);
}
}
}
_ => (),
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn displays_rearranged(
mut messages: MessageReader<Event>,
workspaces: Query<(&LayoutStrip, Entity, Option<&ChildOf>)>,
mut displays: Query<(&mut Display, Entity)>,
window_manager: Res<WindowManager>,
config: Res<Config>,
mut commands: Commands,
) {
for event in messages.read() {
match event {
Event::DisplayAdded { display_id } => {
add_display(
*display_id,
&workspaces,
&window_manager,
&config,
&mut commands,
);
}
Event::DisplayRemoved { display_id } => {
remove_display(*display_id, &workspaces, &mut displays, &mut commands);
}
Event::DisplayMoved { display_id } => {
move_display(
*display_id,
&mut displays,
&window_manager,
&workspaces,
&config,
&mut commands,
);
}
_ => continue,
}
commands.trigger(WMEventTrigger(Event::DisplayChanged));
}
}
fn add_display(
display_id: CGDirectDisplayID,
existing_strips: &Query<(&LayoutStrip, Entity, Option<&ChildOf>)>,
window_manager: &WindowManager,
config: &Config,
commands: &mut Commands,
) {
debug!("Display Added: {display_id:?}");
let Some((mut display, workspace_ids)) = window_manager
.0
.present_displays()
.into_iter()
.find(|(display, _)| display.id() == display_id)
else {
error!("Unable to find added display id {display_id}!");
return;
};
display.set_menubar_height_override(config.menubar_height());
let display_bounds = display.bounds();
let display_entity = commands.spawn(display).id();
reparent_existing_workspaces(
&workspace_ids,
display_entity,
&display_bounds,
existing_strips,
commands,
);
}
fn remove_display(
display_id: CGDirectDisplayID,
workspaces: &Query<(&LayoutStrip, Entity, Option<&ChildOf>)>,
displays: &mut Query<(&mut Display, Entity)>,
commands: &mut Commands,
) {
debug!("Display Removed: {display_id:?}");
let Some((display, display_entity)) = displays
.into_iter()
.find(|(display, _)| display.id() == display_id)
else {
error!("Unable to find removed display!");
return;
};
for (strip, entity, _) in workspaces
.into_iter()
.filter(|(_, _, child)| child.is_some_and(|child| child.parent() == display_entity))
{
let display_id = display.id();
debug!(
"orphaning strip {} after removal of display {display_id}.",
strip.id(),
);
let timeout = Timeout::new(
Duration::from_secs(ORPHANED_SPACES_TIMEOUT_SEC),
Some(format!(
"Orphaned strip {} ({strip}) could not be re-inserted after {ORPHANED_SPACES_TIMEOUT_SEC}s.",
strip.id()
)),
);
if let Ok(mut commands) = commands.get_entity(entity) {
commands.try_insert(timeout);
}
if let Ok(mut commands) = commands.get_entity(display_entity) {
commands.detach_child(entity);
}
}
if let Ok(mut commands) = commands.get_entity(display_entity) {
commands.despawn();
}
}
fn move_display(
display_id: CGDirectDisplayID,
displays: &mut Query<(&mut Display, Entity)>,
window_manager: &Res<WindowManager>,
existing_strips: &Query<(&LayoutStrip, Entity, Option<&ChildOf>)>,
config: &Config,
commands: &mut Commands,
) {
debug!("Display Moved: {display_id:?}");
let Some((mut display, display_entity)) = displays
.iter_mut()
.find(|(display, _)| display.id() == display_id)
else {
error!("Unable to find moved display!");
return;
};
let Some((moved_display, workspace_ids)) = window_manager
.0
.present_displays()
.into_iter()
.find(|(display, _)| display.id() == display_id)
else {
return;
};
*display = moved_display;
display.set_menubar_height_override(config.menubar_height());
reparent_existing_workspaces(
&workspace_ids,
display_entity,
&display.bounds(),
existing_strips,
commands,
);
}
fn reparent_existing_workspaces(
workspace_ids: &[WorkspaceId],
display_entity: Entity,
display_bounds: &IRect,
existing_strips: &Query<(&LayoutStrip, Entity, Option<&ChildOf>)>,
commands: &mut Commands,
) {
for &id in workspace_ids {
let mut found = false;
for (strip, entity, child) in existing_strips {
if strip.id() == id {
found = true;
if child.is_none_or(|child| child.parent() != display_entity) {
if let Ok(mut cmd) = commands.get_entity(entity) {
debug!("reparenting workspace {id} to display {display_entity}");
cmd.try_remove::<Timeout>()
.try_remove::<ChildOf>()
.insert(ChildOf(display_entity));
cmd.insert(RefreshWindowSizes::default());
}
}
}
}
if !found {
let origin = Position(display_bounds.min);
debug!("new workspace {id} on display {display_entity}");
commands.spawn((
origin.clone(),
LayoutStrip::new(id, 0),
SelectedVirtualMarker,
ChildOf(display_entity),
));
}
}
}
#[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:?}"),
}
}
if let Some(config) = initial_config {
commands.insert_resource(config);
}
while let Some(mut process) = initial_processes.pop() {
if process.is_observable() {
debug!("Adding existing process {}", process.name());
commands.spawn((ExistingMarker, process));
} else {
debug!(
"Existing application '{}' is not observable, ignoring it.",
process.name(),
);
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn reposition_dragged_window(
markers: Populated<(&Timeout, &WindowDraggedMarker, Entity)>,
active_workspace: Query<&Scrolling, With<ActiveWorkspaceMarker>>,
mut commands: Commands,
) {
if active_workspace
.iter()
.next()
.is_some_and(|marker| marker.is_user_swiping)
{
for (_, _, marker_entity) in &markers {
commands.entity(marker_entity).despawn();
}
return;
}
for (
timeout,
WindowDraggedMarker {
entity,
display_id: _,
},
_,
) in markers
{
if timeout.timer.is_finished() {
debug!("Window {entity} dragged, refreshing layout.");
reshuffle_around(*entity, &mut commands);
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn update_overlays(
windows: Windows,
applications: Query<&Application>,
active_workspace: Query<(Has<Scrolling>, &LayoutStrip), With<ActiveWorkspaceMarker>>,
overlay_mgr: Option<NonSendMut<OverlayManager>>,
config: Configuration,
) {
use crate::overlay::BorderParams;
use objc2_foundation::{NSPoint, NSRect, NSSize};
let Some(mut overlay_mgr) = overlay_mgr else {
return;
};
let dim_opacity = config.config().dim_inactive_opacity();
let border_enabled = config.config().border_active_window();
let Some((swiping, active_strip)) = active_workspace.iter().next() else {
return;
};
if swiping || config.mission_control_active() || 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_border_radius, detected_border_radius) =
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),
),
));
let Some(app) = windows
.find_parent(window.id())
.and_then(|(_, _, parent)| applications.get(parent).ok())
else {
return;
};
let properties = WindowProperties::new(app, window, config.config());
let focused_border_radius = properties.border_radius();
(
focused_abs_cg,
focused_border_radius,
window.border_radius(),
)
} else {
overlay_mgr.hide_all();
return;
};
let calculated_radius = match config.config().border_radius() {
BorderRadiusOption::Auto => detected_border_radius.unwrap_or(10.0),
BorderRadiusOption::Value(value) => value.max(0.0),
};
let border_params = border_enabled.then(|| BorderParams {
color: config.config().border_color(),
opacity: config.config().border_opacity(),
width: config.config().border_width(),
radius: focused_border_radius.unwrap_or(calculated_radius),
});
let dim_color = config.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 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>,
windows: Windows,
window_manager: Res<WindowManager>,
) {
for _ in exit_events.read() {
info!("Cleaning up before exit");
let ids = windows
.iter()
.map(|(window, _)| window.id())
.collect::<Vec<_>>();
window_manager.dim_windows(&ids, 0.0);
}
}
#[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));
for (entity, FlashMessage(flash), timeout) in messages {
if timeout.timer.is_finished() {
flash_manager.remove();
commands.entity(entity).despawn();
} else {
let opacity = timeout.timer.fraction_remaining();
flash_manager.show(flash, opacity, top_right);
}
}
}