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::{Has, Or, With, Without};
use bevy::ecs::system::{Commands, Local, NonSend, NonSendMut, Populated, Query, Res};
use bevy::tasks::AsyncComputeTaskPool;
use bevy::tasks::futures_lite::future;
use bevy::time::Time;
use log::{debug, error, info, trace, warn};
use objc2_core_graphics::CGDirectDisplayID;
use std::collections::HashSet;
use std::pin::Pin;
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::time::Duration;
use stdext::function_name;
use super::{
ActiveDisplayMarker, BProcess, CommandTrigger, ExistingMarker, FocusedMarker, FreshMarker,
PollForNotifications, RepositionMarker, ResizeMarker, SpawnWindowTrigger, Timeout,
WMEventTrigger,
};
use crate::config::Config;
use crate::ecs::params::{ActiveDisplay, Configuration, DebouncedSystem, Windows};
use crate::ecs::{
ActiveWorkspaceMarker, BruteforceWindows, Initializing, ReshuffleAroundMarker, Unmanaged,
WindowSwipeMarker, reposition_entity, reshuffle_around, resize_entity,
};
use crate::events::Event;
use crate::manager::{
Application, Column, Display, LayoutStrip, Process, Window, WindowManager, WindowOS,
bruteforce_windows,
};
use crate::platform::{PlatformCallbacks, WorkspaceId};
const WINDOW_HIDDEN_THRESHOLD: f64 = 10.0;
#[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::Command { command } => commands.trigger(CommandTrigger(command.clone())),
Event::WindowCreated { element } => {
if let Ok(window) = WindowOS::new(element)
.inspect_err(|err| {
trace!("{}: not adding window {element:?}: {err}", function_name!());
})
.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.",
function_name!()
);
commands.remove_resource::<PollForNotifications>();
}
commands.trigger(WMEventTrigger(event.clone()));
}
Event::WindowTitleChanged { window_id } => {
trace!("{}: WindowTitleChanged: {window_id:?}", function_name!());
}
Event::MenuClosed { window_id } => {
trace!("{}: MenuClosed event: {window_id:?}", function_name!());
}
Event::DisplayResized { display_id } => {
debug!("{}: Display Resized: {display_id:?}", function_name!());
}
Event::DisplayConfigured { display_id } => {
debug!("{}: Display Configured: {display_id:?}", function_name!());
}
Event::SystemWoke { msg } => {
debug!("{}: system woke: {msg:?}", function_name!());
}
_ => 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!", function_name!());
return;
};
for (display, workspaces) in window_manager.present_displays() {
let entity = if display.id() == active_display_id {
commands.spawn((display, ActiveDisplayMarker))
} else {
commands.spawn(display)
}
.id();
let Ok(active_space) = window_manager.active_display_space(active_display_id) else {
return;
};
for id in workspaces {
let strip = LayoutStrip::new(id);
if id == active_space {
commands.spawn((strip, ActiveWorkspaceMarker, ChildOf(entity)));
} else {
commands.spawn((strip, ChildOf(entity)));
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
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 '{}'",
function_name!(),
process.name(),
);
return;
};
commands.spawn((app, ExistingMarker, ChildOf(entity)));
commands.entity(entity).try_remove::<ExistingMarker>();
}
}
#[allow(clippy::needless_pass_by_value)]
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}", function_name!()))
{
offscreen_windows.extend(offscreen);
commands.trigger(SpawnWindowTrigger(found_windows));
}
commands.entity(entity).try_remove::<ExistingMarker>();
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)]
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>)>,
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.",
function_name!(),
windows.iter().len()
);
for (mut strip, active_strip) in &mut workspaces {
let workspace_windows = window_manager
.windows_in_workspace(strip.id())
.inspect_err(|err| {
warn!(
"{}: failed to get windows on workspace {}: {err}",
function_name!(),
strip.id()
);
})
.ok()
.map(|workspace_windows| {
workspace_windows
.into_iter()
.filter_map(|window_id| windows.find_managed(window_id))
.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.index_of(entity).is_err() {
strip.append(entity);
}
}
debug!(
"{}: space {}: after refresh {strip:?}",
function_name!(),
strip.id()
);
if active_strip {
let first_window = strip.first().ok().and_then(|column| column.top());
if let Some(entity) = first_window {
debug!("{}: focusing {entity}", function_name!());
commands.entity(entity).try_insert(FocusedMarker);
}
}
}
commands.remove_resource::<Initializing>();
}
#[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 '{}'",
function_name!(),
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.",
function_name!(),
)),
);
commands.spawn((app, FreshMarker, timeout, ChildOf(entity)));
} else {
debug!(
"{}: failed to register some observers {}",
function_name!(),
process.name()
);
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn add_launched_application(
app_query: Populated<(&mut Application, Entity), With<FreshMarker>>,
windows: Windows,
mut commands: Commands,
) {
let find_window = |window_id| windows.find(window_id);
for (app, entity) 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!", function_name!());
commands.trigger(SpawnWindowTrigger(create_windows));
}
}
}
#[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.",
function_name!(),
);
if let Some(message) = &timeout.message {
debug!("{message}");
}
trace!("{}: Removing timer {entity}", function_name!());
commands.entity(entity).despawn();
} else {
trace!(
"{}: Timer {}",
function_name!(),
timeout.timer.elapsed().as_secs_f32()
);
timeout.timer.tick(clock.delta());
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn find_orphaned_workspaces(
orphans: Populated<(&LayoutStrip, Entity), Without<ChildOf>>,
workspaces: Populated<(&LayoutStrip, Entity, &ChildOf), With<ChildOf>>,
windows: Windows,
displays: Query<&Display>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
let matched_orphans = workspaces.into_iter().filter_map(|(strip, entity, child)| {
orphans.iter().find_map(|(orphan, orphan_entity)| {
(strip.id() == orphan.id()).then_some((
child.parent(),
strip,
entity,
orphan,
orphan_entity,
))
})
});
for (parent_display, strip, entity, orphan, orphan_entity) in matched_orphans {
let Ok(display) = displays.get(parent_display) else {
continue;
};
debug!(
"{}: Re-inserting orphaned strip {} into display {}",
function_name!(),
orphan.id(),
display.id(),
);
if let Ok(mut commands) = commands.get_entity(orphan_entity) {
commands.try_remove::<Timeout>();
}
if let Ok(mut commands) = commands.get_entity(orphan_entity) {
commands.try_insert(ChildOf(parent_display));
}
if let Ok(mut commands) = commands.get_entity(entity) {
commands.try_despawn();
}
let mut in_workspace = window_manager
.windows_in_workspace(strip.id())
.inspect_err(|err| {
warn!("{}: getting windows in workspace: {err}", function_name!());
})
.unwrap_or_default();
for entity in orphan.all_windows() {
if let Some(window) = windows.get(entity) {
let width = display.bounds.size.width * window.width_ratio();
let height = display.bounds.size.height;
debug!(
"{}: refreshing ratio {:.1} for window {}: {:.0}x{:.0}",
function_name!(),
window.width_ratio(),
window.id(),
width,
height,
);
resize_entity(entity, width, height, display.id(), &mut commands);
in_workspace.retain(|window_id| *window_id != window.id());
}
}
let floating = in_workspace.into_iter().filter_map(|window_id| {
windows
.find_all(window_id)
.and_then(|(_, entity, _, unmanaged)| {
matches!(unmanaged, Some(Unmanaged::Floating)).then_some(entity)
})
});
for window_entity in floating {
debug!(
"{}: repositioning floating window {window_entity}",
function_name!(),
);
reposition_entity(window_entity, 0.0, 0.0, display.id(), &mut commands);
}
}
}
#[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 {}.",
function_name!(),
current_display_id,
);
commands.trigger(WMEventTrigger(Event::DisplayChanged));
} else {
debug!(
"{}: new display {} detected.",
function_name!(),
current_display_id
);
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())
{
debug!(
"{}: detected removal of display {}",
function_name!(),
display.id()
);
commands.trigger(WMEventTrigger(Event::DisplayRemoved {
display_id: display.id(),
}));
}
});
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn workspace_change_watcher(
active_display: ActiveDisplay,
window_manager: Res<WindowManager>,
mut current_space: Local<WorkspaceId>,
mut commands: Commands,
) {
let Ok(space_id) = window_manager
.0
.active_display_space(active_display.id())
.inspect_err(|err| warn!("{}: {err}", function_name!()))
else {
return;
};
if *current_space != space_id {
*current_space = space_id;
debug!("{}: workspace changed to {space_id}", function_name!());
commands.trigger(WMEventTrigger(Event::SpaceChanged));
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn animate_windows(
windows: Populated<(&mut Window, Entity, &RepositionMarker)>,
displays: Query<&Display>,
time: Res<Time>,
config: Res<Config>,
mut commands: Commands,
) {
let move_ratio = config.animation_speed() * time.delta_secs_f64();
for (mut window, entity, RepositionMarker { origin, display_id }) in windows {
let Some(display) = displays.iter().find(|display| display.id() == *display_id) else {
continue;
};
let move_delta = (move_ratio * display.bounds.size.width).ceil();
let current = window.frame().origin;
let mut delta_x = (origin.x - current.x).abs().min(move_delta);
let mut delta_y = (origin.y - current.y).abs().min(move_delta);
if delta_x < move_delta && delta_y < move_delta {
commands.entity(entity).try_remove::<RepositionMarker>();
window.reposition(
origin.x,
origin.y.max(display.menubar_height),
&display.bounds,
);
continue;
}
if origin.x < current.x {
delta_x = -delta_x;
}
if origin.y < current.y {
delta_y = -delta_y;
}
trace!(
"{}: window {} dest {:?} delta {move_delta:.0} moving to {:.0}:{:.0}",
function_name!(),
window.id(),
origin,
current.x + delta_x,
current.y + delta_y,
);
window.reposition(
current.x + delta_x,
(current.y + delta_y).max(display.menubar_height),
&display.bounds,
);
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn animate_resize_windows(
windows: Populated<(&mut Window, Entity, &ResizeMarker)>,
displays: Query<&Display>,
time: Res<Time>,
config: Res<Config>,
mut commands: Commands,
) {
let move_ratio = config.animation_speed() * time.delta_secs_f64();
for (mut window, entity, ResizeMarker { size, display_id }) in windows {
let Some(display) = displays.iter().find(|display| display.id() == *display_id) else {
continue;
};
let move_delta = (move_ratio * display.bounds.size.width).ceil();
let current = window.frame().size;
let mut delta_x = (size.width - current.width).abs().min(move_delta);
let mut delta_y = (size.height - current.height).abs().min(move_delta);
if delta_x < move_delta && delta_y < move_delta {
commands.entity(entity).try_remove::<ResizeMarker>();
window.resize(size.width, size.height, &display.bounds);
continue;
}
if size.width < current.width {
delta_x = -delta_x;
}
if size.height < current.height {
delta_y = -delta_y;
}
trace!(
"{}: window {} size {:?} delta {move_delta:.0} resizing to {:.0}:{:.0}",
function_name!(),
window.id(),
size,
current.width + delta_x,
current.height + delta_y,
);
window.resize(
current.width + delta_x,
current.height + delta_y,
&display.bounds,
);
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn window_swiper(
sliding: Populated<(&Window, Entity, &WindowSwipeMarker)>,
windows: Windows,
active_display: ActiveDisplay,
config: Configuration,
mut debouncer: DebouncedSystem,
mut commands: Commands,
) {
const DEBOUNCE_SWIPE_EVENTS_MS: u64 = 500;
for (window, entity, WindowSwipeMarker(delta)) in sliding {
commands.entity(entity).try_remove::<WindowSwipeMarker>();
let pos_x = window.frame().origin.x - (active_display.bounds().size.width * delta);
let frame = window.frame();
reposition_entity(
entity,
pos_x
.min(active_display.bounds().size.width - frame.size.width)
.max(0.0),
frame.origin.y,
active_display.id(),
&mut commands,
);
if pos_x > 0.0 && pos_x < (active_display.bounds().size.width - frame.size.width) {
reshuffle_around(entity, &mut commands);
return;
}
if !config.continuous_swipe()
|| debouncer.bounce(Duration::from_millis(DEBOUNCE_SWIPE_EVENTS_MS))
{
return;
}
if let Some(window) =
slide_to_next_window(&active_display, entity, *delta, pos_x, &mut commands)
.and_then(|entity| windows.get(entity))
{
commands.trigger(WMEventTrigger(Event::WindowFocused {
window_id: window.id(),
}));
}
}
}
fn slide_to_next_window(
active_display: &ActiveDisplay,
entity: Entity,
delta: f64,
delta_x: f64,
commands: &mut Commands,
) -> Option<Entity> {
let strip = active_display.active_strip();
let neighbour = if delta_x < 0.0 {
strip.right_neighbour(entity)
} else {
strip.left_neighbour(entity)
};
neighbour.inspect(|neighbour| {
debug!(
"{}: switching to {neighbour} with delta {delta}",
function_name!()
);
reshuffle_around(*neighbour, commands);
})
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn reshuffle_around_window(
active_display: ActiveDisplay,
marker: Populated<Entity, With<ReshuffleAroundMarker>>,
windows: Query<(&Window, Option<&RepositionMarker>, Option<&ResizeMarker>)>,
mut commands: Commands,
) {
let display_bounds = active_display.bounds();
let active_strip = active_display.active_strip();
for entity in marker {
if let Ok(mut cmd) = commands.get_entity(entity) {
cmd.try_remove::<ReshuffleAroundMarker>();
}
let Ok((window, moving, resizing)) = windows.get(entity) else {
continue;
};
let frame = window.expose_window(&active_display, moving, resizing, entity, &mut commands);
let window_width = |entity| {
windows.get(entity).ok().map(|(window, _, resizing)| {
resizing.map_or(window.frame().size.width, |marker| marker.size.width)
})
};
let Some(positions) = calculate_positions(
entity,
frame.origin.x,
display_bounds.size.width,
active_strip,
&window_width,
) else {
return;
};
let positions =
positions
.zip(active_strip.all_columns())
.filter_map(|(position, entity)| {
window_width(entity).map(|width| (position, entity, width))
});
for (upper_left, entity, width) in positions {
let Ok(column) = active_strip
.index_of(entity)
.and_then(|index| active_strip.get(index))
else {
continue;
};
reposition_stack(
upper_left,
&column,
width,
&active_display,
&windows,
&mut commands,
);
}
}
}
pub fn absolute_positions<W>(
active_strip: &LayoutStrip,
window_width: W,
) -> impl Iterator<Item = f64>
where
W: Fn(Entity) -> Option<f64>,
{
let mut left_edge = 0.0;
active_strip
.all_columns()
.into_iter()
.filter_map(window_width)
.map(move |width| {
let temp = left_edge;
left_edge += width;
temp
})
}
fn calculate_positions<W>(
entity: Entity,
current_x: f64,
display_width: f64,
active_strip: &LayoutStrip,
window_width: W,
) -> Option<impl Iterator<Item = f64>>
where
W: Fn(Entity) -> Option<f64>,
{
let widths = active_strip
.all_columns()
.into_iter()
.filter_map(&window_width)
.collect::<Vec<_>>();
let positions = absolute_positions(active_strip, &window_width).collect::<Vec<_>>();
let offset = active_strip
.index_of(entity)
.ok()
.and_then(|index| positions.get(index))
.map(|offset| current_x - offset)?;
Some(
positions
.into_iter()
.zip(widths)
.map(move |(position, width)| {
let left_edge = position + offset;
if left_edge + width < 0.0 {
0.0 - width + WINDOW_HIDDEN_THRESHOLD
} else if left_edge > display_width - WINDOW_HIDDEN_THRESHOLD {
display_width - WINDOW_HIDDEN_THRESHOLD
} else {
left_edge
}
}),
)
}
fn reposition_stack(
upper_left: f64,
column: &Column,
width: f64,
active_display: &ActiveDisplay,
windows: &Query<(&Window, Option<&RepositionMarker>, Option<&ResizeMarker>)>,
commands: &mut Commands,
) {
const MIN_WINDOW_HEIGHT: f64 = 200.0;
let display_height =
active_display.bounds().size.height - active_display.display().menubar_height;
let entities = match column {
Column::Single(entity) => vec![*entity],
Column::Stack(stack) => stack.clone(),
};
let heights = entities
.iter()
.filter_map(|entity| {
windows.get(*entity).ok().map(|(window, _, resizing)| {
resizing.map_or(window.frame().size.height, |marker| marker.size.height)
})
})
.collect::<Vec<_>>();
if heights.len() != entities.len() {
warn!("{}: Mismatch in heights and entities.", function_name!());
return;
}
let Some(heights) = binpack_heights(&heights, MIN_WINDOW_HEIGHT, display_height) else {
info!("{}: Unable to fit all windows.", function_name!());
return;
};
let display_width = active_display.bounds().size.width;
let other_display = active_display.other().next();
let display_above = other_display.is_some_and(|other_display| {
active_display.bounds().origin.x < other_display.bounds.origin.x
});
let visible_window = !display_above
|| upper_left + width > WINDOW_HIDDEN_THRESHOLD
&& upper_left < display_width - WINDOW_HIDDEN_THRESHOLD;
let mut y_pos = if visible_window {
0.0
} else {
display_height / 4.0
};
for (entity, window_height) in entities.into_iter().zip(heights) {
reposition_entity(entity, upper_left, y_pos, active_display.id(), commands);
resize_entity(entity, width, window_height, active_display.id(), commands);
if visible_window {
y_pos += window_height;
}
}
}
fn binpack_heights(heights: &[f64], min_height: f64, total_height: f64) -> Option<Vec<f64>> {
let mut count = heights.len();
let mut output = vec![];
loop {
let mut idx = 0;
let mut remaining = total_height;
while idx < count {
let remaining_windows = u32::try_from(heights.len() - idx).unwrap();
if heights[idx] < remaining {
if idx + 1 == count {
output.push(remaining);
} else {
output.push(heights[idx]);
}
remaining -= heights[idx];
} else if remaining >= min_height * f64::from(remaining_windows) {
output.push(remaining);
remaining = 0.0;
} else {
break;
}
idx += 1;
}
if idx == count {
break;
}
count -= 1;
output.clear();
}
let remaining = heights.len() - count;
if remaining > 0 {
count -= 1;
output.truncate(count);
let sum = output.iter().fold(0.0, |acc, height| acc + height);
let avg_height =
((total_height - sum) / f64::from(u32::try_from(remaining + 1).unwrap())).floor();
if avg_height < min_height {
return None;
}
while count < heights.len() {
output.push(avg_height);
count += 1;
}
}
Some(output)
}
#[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)>,
active_display: ActiveDisplay,
mut commands: Commands,
) {
for event in messages.read() {
match event {
Event::WindowMoved { window_id } | Event::WindowResized { window_id } => {
if let Some((mut window, entity)) = windows
.iter_mut()
.find(|(window, _)| window.id() == *window_id)
{
if active_display.active_strip().index_of(entity).is_err() {
continue;
}
_ = window.update_frame(&active_display.bounds());
if matches!(event, Event::WindowResized { window_id: _ }) {
reshuffle_around(entity, &mut commands);
}
}
}
_ => (),
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn displays_rearranged(
mut messages: MessageReader<Event>,
workspaces: Query<(&LayoutStrip, Entity, &ChildOf)>,
mut displays: Query<(&mut Display, Entity)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
for event in messages.read() {
match event {
Event::DisplayAdded { display_id } => {
add_display(*display_id, &window_manager, &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);
}
_ => continue,
}
commands.trigger(WMEventTrigger(Event::DisplayChanged));
}
}
fn add_display(
display_id: CGDirectDisplayID,
window_manager: &Res<WindowManager>,
commands: &mut Commands,
) {
debug!("{}: Display Added: {display_id:?}", function_name!());
let Some((display, workspaces)) = window_manager
.0
.present_displays()
.into_iter()
.find(|(display, _)| display.id() == display_id)
else {
error!(
"{}: Unable to find added display id {display_id}!",
function_name!()
);
return;
};
let children = workspaces
.into_iter()
.map(|id| commands.spawn(LayoutStrip::new(id)).id())
.collect::<Vec<_>>();
commands.spawn(display).add_children(&children);
}
fn remove_display(
display_id: CGDirectDisplayID,
workspaces: &Query<(&LayoutStrip, Entity, &ChildOf)>,
displays: &mut Query<(&mut Display, Entity)>,
commands: &mut Commands,
) {
const ORPHANED_SPACES_TIMEOUT_SEC: u64 = 30;
debug!("{}: Display Removed: {display_id:?}", function_name!());
let Some((display, display_entity)) = displays
.into_iter()
.find(|(display, _)| display.id() == display_id)
else {
error!("{}: Unable to find removed display!", function_name!());
return;
};
for (strip, entity, _) in workspaces
.into_iter()
.filter(|(_, _, child)| child.parent() == display_entity)
{
debug!(
"{}: orphaning strip {} after removal of display {}.",
function_name!(),
strip.id(),
display.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.",
function_name!(),
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>,
) {
debug!("{}: Display Moved: {display_id:?}", function_name!());
let Some((mut display, _)) = displays
.iter_mut()
.find(|(display, _)| display.id() == display_id)
else {
error!("{}: Unable to find moved display!", function_name!());
return;
};
let Some((moved_display, _)) = window_manager
.0
.present_displays()
.into_iter()
.find(|(display, _)| display.id() == display_id)
else {
return;
};
*display = moved_display;
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn gather_initial_processes(
receiver: Option<NonSendMut<Receiver<Event>>>,
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) => {
initial_config = Some(config);
}
event => warn!(
"{}: Stray event during initial process gathering: {event:?}",
function_name!()
),
}
}
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 {}",
function_name!(),
process.name()
);
commands.spawn((ExistingMarker, process));
} else {
debug!(
"{}: Existing application '{}' is not observable, ignoring it.",
function_name!(),
process.name(),
);
}
}
}
#[test]
fn test_binpack() {
const MIN_HEIGHT: f64 = 100.0;
let heights = [300.0, 300.0, 300.0, 300.0];
let out = binpack_heights(&heights, MIN_HEIGHT, 1500.0).unwrap();
assert_eq!(out, vec![300.0, 300.0, 300.0, 600.0]);
let out = binpack_heights(&heights, MIN_HEIGHT, 1024.0).unwrap();
assert_eq!(out, vec![300.0, 300.0, 300.0, 124.0]);
let out = binpack_heights(&heights, MIN_HEIGHT, 800.0).unwrap();
assert_eq!(out, vec![300.0, 300.0, 100.0, 100.0]);
let out = binpack_heights(&heights, MIN_HEIGHT, 440.0).unwrap();
assert_eq!(out, vec![110.0, 110.0, 110.0, 110.0]);
let out = binpack_heights(&heights, MIN_HEIGHT, 390.0);
assert_eq!(out, None);
}