use bevy::ecs::entity::Entity;
use bevy::ecs::hierarchy::{ChildOf, Children};
use bevy::ecs::message::{MessageReader, MessageWriter};
use bevy::ecs::query::{Has, Or, With};
use bevy::ecs::system::{Commands, Local, Populated, Query, Res, ResMut};
use bevy::ecs::world::World;
use bevy::time::Time;
use log::{debug, error, info, trace, warn};
use std::time::Duration;
use stdext::function_name;
use super::{
ActiveDisplayMarker, BProcess, CommandTrigger, ExistingMarker, FocusedMarker, FreshMarker,
OrphanedPane, PollForNotifications, RepositionMarker, ResizeMarker, SpawnWindowTrigger,
StrayFocusEvent, Timeout, Unmanaged, WMEventTrigger,
};
use crate::config::Config;
use crate::ecs::params::{
ActiveDisplay, ActiveDisplayMut, Configuration, DebouncedSystem, ThrottledSystem,
};
use crate::ecs::{
ReshuffleAroundMarker, WindowSwipeMarker, reposition_entity, reshuffle_around, resize_entity,
};
use crate::events::Event;
use crate::manager::{Application, Display, Panel, Window, WindowManager, WindowOS, WindowPane};
const WINDOW_HIDDEN_THRESHOLD: f64 = 10.0;
#[allow(clippy::needless_pass_by_value)]
pub(super) fn dispatch_toplevel_triggers(
mut messages: MessageReader<Event>,
mut broken_notifications: ResMut<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.0 {
broken_notifications.0 = false;
info!(
"{}: Workspace and display notifications arriving correctly. Disabling the polling.",
function_name!()
);
}
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())),
}
}
}
pub fn run_initial_oneshot_systems(world: &mut World) {
let existing_apps_setup = [
world.register_system(add_existing_process),
world.register_system(add_existing_application),
world.register_system(finish_setup),
];
let init = existing_apps_setup
.into_iter()
.map(|id| world.run_system(id))
.collect::<std::result::Result<Vec<()>, _>>();
if let Err(err) = init {
error!("{}: Error running initial systems: {err}", function_name!());
}
}
#[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 in window_manager.present_displays() {
if display.id() == active_display_id {
commands.spawn((display, ActiveDisplayMarker));
} else {
commands.spawn(display);
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn add_existing_process(
window_manager: Res<WindowManager>,
process_query: Query<(Entity, &BProcess), With<ExistingMarker>>,
mut commands: Commands,
) {
for (entity, process) in process_query {
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)]
fn add_existing_application(
window_manager: Res<WindowManager>,
displays: Query<&Display>,
app_query: Query<(&mut Application, Entity), With<ExistingMarker>>,
mut commands: Commands,
) {
let spaces = displays
.iter()
.flat_map(|display| display.spaces.keys().copied().collect::<Vec<_>>())
.collect::<Vec<_>>();
for (mut app, entity) in app_query {
if app.observe().is_ok_and(|result| result)
&& let Ok(windows) = window_manager
.add_existing_application_windows(&mut app, &spaces, 0)
.inspect_err(|err| warn!("{}: {err}", function_name!()))
{
commands.trigger(SpawnWindowTrigger(windows));
}
commands.entity(entity).try_remove::<ExistingMarker>();
}
}
#[allow(clippy::needless_pass_by_value)]
fn finish_setup(
mut windows: Query<(&mut Window, Entity, Has<Unmanaged>)>,
displays: Query<(&mut Display, Has<ActiveDisplayMarker>)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
info!(
"{}: Finished Initialization: found {} windows.",
function_name!(),
windows.iter().len()
);
for (mut display, active) in displays {
window_manager.refresh_display(&mut display, &mut windows);
if active {
let active_panel = window_manager
.active_display_space(display.id())
.and_then(|active_space| display.active_panel(active_space));
let first_window = active_panel
.ok()
.and_then(|panel| panel.first().ok())
.and_then(|panel| panel.top());
if let Some(entity) = first_window {
debug!("{}: focusing {entity}", function_name!());
commands.entity(entity).try_insert(FocusedMarker);
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn add_launched_process(
window_manager: Res<WindowManager>,
process_query: Populated<(Entity, &mut BProcess, Has<Children>), With<FreshMarker>>,
mut commands: Commands,
) {
const APP_OBSERVABLE_TIMEOUT_SEC: u64 = 5;
for (entity, mut process, children) in process_query {
let process = &mut *process.0;
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()
);
}
}
}
pub(super) fn add_launched_application(
app_query: Populated<(&mut Application, Entity), With<FreshMarker>>,
windows: Query<&Window>,
mut commands: Commands,
) {
let find_window = |window_id| windows.iter().find(|window| window.id() == window_id);
for (app, entity) in app_query {
let Ok(app_windows) = app.window_list() else {
continue;
};
let create_windows = app_windows
.into_iter()
.filter_map(|window| {
window
.inspect_err(|err| warn!("{}: error adding window: {err}", function_name!()))
.ok()
.and_then(|window| {
find_window(window.id()).is_none().then_some(window)
})
})
.collect();
commands.entity(entity).try_remove::<FreshMarker>();
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());
}
}
}
pub(super) fn retry_stray_focus(
focus_events: Populated<(Entity, &StrayFocusEvent)>,
windows: Query<&Window>,
mut messages: MessageWriter<Event>,
mut commands: Commands,
) {
for (timeout_entity, stray_focus) in focus_events {
let window_id = stray_focus.0;
if windows.iter().any(|window| window.id() == window_id) {
debug!(
"{}: Re-queueing lost focus event for window id {window_id}.",
function_name!()
);
messages.write(Event::WindowFocused { window_id });
commands.entity(timeout_entity).despawn();
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn find_orphaned_spaces(
orphaned_spaces: Populated<(Entity, &mut OrphanedPane)>,
mut active_display: ActiveDisplayMut,
mut commands: Commands,
) {
let active_display_id = active_display.id();
for (entity, orphan_pane) in orphaned_spaces {
debug!(
"{}: Checking orphaned pane {}",
function_name!(),
orphan_pane.id
);
for (space_id, pane) in &mut active_display.display().spaces {
if *space_id == orphan_pane.id {
debug!(
"{}: Re-inserting orphaned pane {} into display {}",
function_name!(),
orphan_pane.id,
active_display_id
);
for window_entity in orphan_pane.pane.all_windows() {
pane.append(window_entity);
}
commands.entity(entity).despawn();
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn display_changes_watcher(
displays: Query<(&Display, Has<ActiveDisplayMarker>)>,
window_manager: Res<WindowManager>,
mut throttle: ThrottledSystem,
mut commands: Commands,
) {
const DISPLAY_CHANGE_CHECK_FREQ_MS: u64 = 1000;
if throttle.throttled(Duration::from_millis(DISPLAY_CHANGE_CHECK_FREQ_MS)) {
return;
}
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 throttle: ThrottledSystem,
mut current_space: Local<u64>,
mut commands: Commands,
) {
const WORKSPACE_CHANGE_FREQ_MS: u64 = 1000;
if throttle.throttled(Duration::from_millis(WORKSPACE_CHANGE_FREQ_MS)) {
return;
}
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_speed = config
.options()
.animation_speed
.unwrap_or(1_000_000.0)
.max(500.0);
let move_delta = move_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 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_speed = config
.options()
.animation_speed
.unwrap_or(1_000_000.0)
.max(500.0);
let move_delta = move_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 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: Query<(&Window, Entity)>,
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).ok())
{
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 Ok(pane) = active_display.active_panel() else {
return None;
};
let neighbour = if delta_x < 0.0 {
pane.right_neighbour(entity)
} else {
pane.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>)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
if window_manager
.0
.active_display_id()
.is_ok_and(|id| active_display.id() != id)
{
debug!("{}: detected display change.", function_name!());
commands.trigger(WMEventTrigger(Event::DisplayChanged));
return;
}
let display_bounds = active_display.bounds();
let Ok(active_panel) = active_display.active_panel() else {
return;
};
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_panel,
&window_width,
) else {
return;
};
let positions =
positions
.zip(active_panel.all_columns())
.filter_map(|(position, entity)| {
window_width(entity).map(|width| (position, entity, width))
});
for (upper_left, entity, width) in positions {
let Ok(panel) = active_panel
.index_of(entity)
.and_then(|index| active_panel.get(index))
else {
continue;
};
reposition_stack(
upper_left,
&panel,
width,
&active_display,
&windows,
&mut commands,
);
}
}
}
pub fn absolute_positions<W>(
active_panel: &WindowPane,
window_width: W,
) -> impl Iterator<Item = f64>
where
W: Fn(Entity) -> Option<f64>,
{
let mut left_edge = 0.0;
active_panel
.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_panel: &WindowPane,
window_width: W,
) -> Option<impl Iterator<Item = f64>>
where
W: Fn(Entity) -> Option<f64>,
{
let widths = active_panel
.all_columns()
.into_iter()
.filter_map(&window_width)
.collect::<Vec<_>>();
let positions = absolute_positions(active_panel, &window_width).collect::<Vec<_>>();
let offset = active_panel
.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,
panel: &Panel,
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 panel {
Panel::Single(entity) => vec![*entity],
Panel::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 mut y_pos = 0f64;
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);
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)
}
#[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);
}