use bevy::ecs::change_detection::DetectChangesMut;
use bevy::ecs::entity::Entity;
use bevy::ecs::hierarchy::{ChildOf, Children};
use bevy::ecs::lifecycle::{Add, Remove, RemovedComponents};
use bevy::ecs::message::{MessageReader, MessageWriter};
use bevy::ecs::observer::On;
use bevy::ecs::query::{Added, Has, With, Without};
use bevy::ecs::system::{Commands, NonSendMut, ParamSet, Populated, Query, Res, ResMut, Single};
use bevy::math::IRect;
use notify::event::{DataChange, MetadataKind, ModifyKind};
use notify::{EventKind, Watcher};
use std::cmp::Ordering;
use std::time::Duration;
use tracing::{Level, debug, error, info, instrument, trace, warn};
use super::{
ActiveDisplayMarker, BProcess, FocusedMarker, FreshMarker, MissionControlActive,
PreviousManagedStrip, RetryFrontSwitch, SpawnWindowTrigger, StrayFocusEvent, SystemTheme,
Timeout, Unmanaged,
};
use crate::config::Config;
use crate::ecs::focus::FocusHistory;
use crate::ecs::layout::LayoutStrip;
use crate::ecs::params::{ActiveDisplay, GlobalState, WindowCtx, Windows};
use crate::ecs::state::PaneruState;
use crate::ecs::workspace::RestoreFocusMarker;
use crate::ecs::{
ActiveWorkspaceMarker, Bounds, DockPosition, Initializing, LayoutPosition, Position,
ResizeMarker, RestoreWindowState, Scrolling, SendMessageTrigger, SpawnCommandsExt,
VerifyWindowPosition, WidthRatio, WindowProperties,
};
use crate::events::{DestroySource, Event};
use crate::manager::{
Application, Display, Origin, Process, Size, Window, WindowManager, WindowPadding,
};
use crate::platform::WinID;
use crate::util::{round_px, symlink_target};
type ActiveDisplayViewport<'w, 's> =
Single<'w, 's, (&'static Display, Option<&'static DockPosition>), With<ActiveDisplayMarker>>;
type AddedWindowDefaults<'w, 's> = Populated<
'w,
's,
(
&'static mut Window,
&'static mut Position,
&'static mut Bounds,
&'static mut WidthRatio,
&'static ChildOf,
),
Added<Window>,
>;
type WindowDefaultsQueries<'w, 's> = ParamSet<
'w,
's,
(
AddedWindowDefaults<'static, 'static>,
Query<'static, 'static, &'static WidthRatio, With<FocusedMarker>>,
),
>;
fn update_passthrough(window: &Window, app: &Application, config: &Config) {
let properties = WindowProperties::new(app, window, config);
crate::platform::input::set_focused_passthrough(properties.passthrough_keys());
}
pub(crate) fn apply_config_side_effects(
config: &Config,
displays: &mut Query<&mut Display>,
windows: &Windows,
applications: &Query<&Application>,
) {
let height = config.menubar_height();
for mut display in &mut *displays {
display.set_menubar_height_override(height);
}
if let Some((window, _, parent)) = windows
.focused()
.and_then(|(w, e)| windows.find_parent(w.id()).map(|(w, _, p)| (w, e, p)))
&& let Ok(app) = applications.get(parent)
{
update_passthrough(window, app, config);
}
}
pub(super) fn front_switched_trigger(
mut messages: MessageReader<Event>,
processes: Query<(&BProcess, &Children)>,
applications: Query<&Application>,
window_manager: Res<WindowManager>,
mut config: GlobalState,
mut commands: Commands,
) {
const FRONT_SWITCH_RETRY_SEC: u64 = 2;
for event in messages.read() {
let Event::ApplicationFrontSwitched { psn } = event else {
continue;
};
let Some((BProcess(process), children)) =
processes.iter().find(|process| &process.0.psn() == psn)
else {
debug!("Unable to find process with PSN {psn:?}");
continue;
};
if children.len() > 1 {
warn!("Multiple apps registered to process '{}'.", process.name());
}
let Some(&app_entity) = children.first() else {
error!("No application for process '{}'.", process.name());
continue;
};
let Some(app) = applications.get(app_entity).ok() else {
error!("No application for process '{}'.", process.name());
continue;
};
debug!("front switching process: {}", process.name());
if let Ok(focused_id) = app.focused_window_id().inspect_err(|err| {
warn!("can not get current focus: {err}");
}) {
if let Some(point) = window_manager.cursor_position()
&& window_manager
.find_window_at_point(&point)
.is_ok_and(|window_id| window_id != focused_id)
{
config.set_skip_reshuffle(false);
config.set_ffm_flag(None);
}
commands.trigger(SendMessageTrigger(Event::WindowFocused {
window_id: focused_id,
}));
} else {
let timeout = Timeout::new(
Duration::from_secs(FRONT_SWITCH_RETRY_SEC),
Some(format!(
"Front switch retry for '{}' timed out.",
process.name()
)),
&mut commands,
);
commands.spawn((timeout, RetryFrontSwitch(app_entity)));
}
}
}
pub(super) fn theme_change_trigger(
mut messages: MessageReader<Event>,
windows: Windows,
window_manager: Res<WindowManager>,
config: Res<Config>,
mut theme: Option<ResMut<SystemTheme>>,
) {
for event in messages.read() {
let Event::ThemeChanged = event else {
continue;
};
let Some(ref mut theme) = theme else {
continue;
};
let is_dark = crate::util::is_dark_mode();
if theme.is_dark == is_dark {
continue;
}
theme.is_dark = is_dark;
info!("System theme changed: dark_mode={is_dark}");
let Some(dim_ratio) = config.window_dim_ratio(is_dark) else {
continue;
};
let focused_id = windows.focused().map(|(window, _)| window.id());
let windows_to_dim: Vec<WinID> = windows
.iter()
.filter(|(window, _)| Some(window.id()) != focused_id)
.map(|(window, _)| window.id())
.collect();
if !windows_to_dim.is_empty() {
window_manager.dim_windows(&windows_to_dim, dim_ratio);
}
}
}
#[instrument(level = Level::DEBUG, skip_all)]
#[allow(clippy::too_many_lines)]
pub(super) fn window_focused_trigger(
mut messages: MessageReader<Event>,
applications: Query<&Application>,
mut workspaces: Query<(Entity, &mut LayoutStrip, Has<ActiveWorkspaceMarker>)>,
restore_guards: Query<(Entity, &RestoreFocusMarker)>,
mut focus_history: ResMut<FocusHistory>,
global_state: GlobalState,
mut ctx: WindowCtx,
) {
const STRAY_FOCUS_RETRY_SEC: u64 = 2;
let initial_focused = ctx
.windows
.focused()
.map(|(window, entity)| (entity, window.id()));
let mut current_focused = initial_focused;
for event in messages.read() {
let Event::WindowFocused { window_id } = *event else {
continue;
};
let Some((window, entity, parent)) = ctx.windows.find_parent(window_id) else {
let timeout = Timeout::new(
Duration::from_secs(STRAY_FOCUS_RETRY_SEC),
None,
&mut ctx.commands,
);
ctx.commands.spawn((timeout, StrayFocusEvent(window_id)));
continue;
};
let Ok(app) = applications.get(parent) else {
warn!("Unable to get parent for window {}.", window.id());
continue;
};
if !app.is_frontmost() {
continue;
}
let (window, entity, window_id) = match app.focused_window_id() {
Ok(current) if current != window_id => {
match ctx.windows.find_parent(current) {
Some((current_window, current_entity, current_parent))
if current_parent == parent =>
{
debug!(
"app {} reports window {current} focused, not {window_id}; following it",
app.name()
);
(current_window, current_entity, current)
}
_ => continue,
}
}
_ => (window, entity, window_id),
};
update_passthrough(window, app, &ctx.config);
let already_focused = current_focused.is_some_and(|(_, id)| id == window_id);
let managed = ctx
.windows
.get_managed(entity)
.and_then(|(_, _, managed)| managed);
if matches!(managed, Some(Unmanaged::Hidden)) {
if let Ok(mut entity_commands) = ctx.commands.get_entity(entity) {
entity_commands.try_remove::<Unmanaged>();
}
ctx.commands
.trigger(SendMessageTrigger(Event::WindowFocused { window_id }));
continue;
}
let mut owner = None;
let mut owning_workspace_id = None;
let mut active_workspace_id = None;
for (strip_entity, mut strip, active) in &mut workspaces {
if active {
active_workspace_id = Some(strip.id());
}
if owner.is_none() && strip.contains(entity) {
if let Ok(index) = strip.index_of(entity)
&& let Some(column) = strip.get_column_mut(index)
{
column.move_to_front(entity);
}
owning_workspace_id = Some(strip.id());
owner = Some((strip_entity, active));
}
}
if owner.is_none() && managed.is_none() {
continue;
}
if let Some((strip_entity, active)) = owner
&& !active
&& let Ok(mut entity_commands) = ctx.commands.get_entity(strip_entity)
{
entity_commands.try_insert(ActiveWorkspaceMarker);
}
if let Some(workspace_id) = owning_workspace_id.or(active_workspace_id) {
let unmanaged = ctx.windows.get_managed(entity).and_then(|(_, _, u)| u);
focus_history.record(workspace_id, entity, unmanaged);
}
let mut restored_focus = false;
for (guard_entity, guard) in &restore_guards {
if guard.entity == entity {
restored_focus = true;
} else if let Ok(mut entity_commands) = ctx.commands.get_entity(guard_entity) {
entity_commands.try_despawn();
}
}
if already_focused {
if restored_focus {
continue;
}
if !global_state.skip_reshuffle() && !global_state.initializing() {
ctx.commands.ensure_visible(entity);
}
continue;
}
current_focused = Some((entity, window_id));
}
if let Some((entity, window_id)) = current_focused
&& current_focused != initial_focused
&& let Ok(mut entity_commands) = ctx.commands.get_entity(entity)
{
entity_commands.try_insert(FocusedMarker);
debug!("window {window_id} ({entity}) focused.");
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
pub(super) fn mission_control_trigger(
mut messages: MessageReader<Event>,
windows: Windows,
mut workspaces: Query<(
Entity,
&mut LayoutStrip,
Has<ActiveWorkspaceMarker>,
Option<&Scrolling>,
)>,
mut mission_control_active: ResMut<MissionControlActive>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
for event in messages.read() {
match event {
Event::MissionControlShowAllWindows
| Event::MissionControlShowFrontWindows
| Event::MissionControlShowDesktop => {
mission_control_active.as_mut().0 = true;
for (entity, _, _, scroll) in &workspaces {
if scroll.is_some()
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_remove::<Scrolling>();
}
}
}
Event::MissionControlExit => {
mission_control_active.as_mut().0 = false;
if let Some(mut active_strip) = workspaces
.iter_mut()
.find_map(|(_, strip, active, _)| active.then_some(strip))
&& let Ok(present_windows) =
window_manager.windows_in_workspace(active_strip.id())
{
let moved_windows = active_strip
.all_windows()
.into_iter()
.filter_map(|entity| windows.get(entity).zip(Some(entity)))
.filter(|(window, _)| !present_windows.contains(&window.id()));
for (window, entity) in moved_windows {
debug!(
"window {} {entity} moved, removing from workspace {}",
window.id(),
active_strip.id(),
);
active_strip.remove(entity);
}
}
}
_ => (),
}
}
}
pub(super) fn application_event_trigger(
mut messages: MessageReader<Event>,
processes: Query<(&BProcess, Entity)>,
mut commands: Commands,
) {
const PROCESS_READY_TIMEOUT_SEC: u64 = 5;
let find_process = |psn| {
processes
.iter()
.find(|(BProcess(process), _)| process.psn() == psn)
};
for event in messages.read() {
match event {
Event::ApplicationLaunched { psn, observer } if find_process(*psn).is_none() => {
let process: BProcess = Process::new(psn, observer.clone()).into();
if process.pid() == 0 {
debug!("Skipping process with PID 0 (likely kernel_task).");
continue;
}
let timeout = Timeout::new(
Duration::from_secs(PROCESS_READY_TIMEOUT_SEC),
Some(format!(
"Process '{}' did not become ready in {PROCESS_READY_TIMEOUT_SEC}s.",
process.name()
)),
&mut commands,
);
commands.spawn((FreshMarker, timeout, process));
}
Event::ApplicationTerminated { psn } => {
if let Some((_, entity)) = find_process(*psn)
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_despawn();
}
}
_ => (),
}
}
}
pub(super) fn dispatch_application_messages(
mut messages: MessageReader<Event>,
windows: Windows,
applications: Query<(&Application, &Children)>,
unmanaged_query: Query<&Unmanaged>,
mut commands: Commands,
) {
let find_window = |window_id| windows.find(window_id);
for event in messages.read() {
match event {
Event::WindowMinimized { window_id } => {
if let Some((_, entity)) = find_window(*window_id)
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_insert(Unmanaged::Minimized);
}
}
Event::WindowDeminimized { window_id } => {
if let Some((_, entity)) = find_window(*window_id)
&& matches!(unmanaged_query.get(entity), Ok(Unmanaged::Minimized))
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_remove::<Unmanaged>();
}
}
Event::ApplicationHidden { pid } => {
let Some((_, children)) = applications.iter().find(|(app, _)| app.pid() == *pid)
else {
warn!("Unable to find with pid {pid}");
continue;
};
for entity in children {
if unmanaged_query.get(*entity).is_err()
&& let Ok(mut entity_commands) = commands.get_entity(*entity)
{
entity_commands.try_insert(Unmanaged::Hidden);
}
}
}
Event::ApplicationVisible { pid } => {
let Some((_, children)) = applications.iter().find(|(app, _)| app.pid() == *pid)
else {
warn!("Unable to find application with pid {pid}");
continue;
};
for entity in children {
if matches!(unmanaged_query.get(*entity), Ok(Unmanaged::Hidden))
&& let Ok(mut entity_commands) = commands.get_entity(*entity)
{
entity_commands.try_remove::<Unmanaged>();
}
}
}
_ => (),
}
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
pub(super) fn window_unmanaged_trigger(
trigger: On<Add, Unmanaged>,
apps: Query<(Entity, &Application)>,
mut workspaces: Query<(&mut LayoutStrip, Has<ActiveWorkspaceMarker>)>,
active_display: Option<ActiveDisplayViewport>,
initializing: Option<Res<Initializing>>,
mut ctx: WindowCtx,
) {
const UNMANAGED_MAX_SCREEN_RATIO_NUM: i32 = 4;
const UNMANAGED_MAX_SCREEN_RATIO_DEN: i32 = 5;
const UNMANAGED_POP_OFFSET: i32 = 32;
fn clamp_origin_to_bounds(origin: IRect, size: Size, bounds: IRect) -> IRect {
let max = (bounds.max - size).max(bounds.min);
let min = origin.min.clamp(bounds.min, max);
IRect::from_corners(min, min + size)
}
fn offset_frame_within_bounds(frame: IRect, bounds: IRect, offset: i32) -> IRect {
let candidates = [
(offset, offset),
(offset, -offset),
(-offset, offset),
(-offset, -offset),
(offset, 0),
(-offset, 0),
(0, offset),
(0, -offset),
];
for (dx, dy) in candidates {
let moved = IRect::from_corners(
Origin::new(frame.min.x + dx, frame.min.y + dy),
Origin::new(frame.max.x + dx, frame.max.y + dy),
);
if moved.min.x >= bounds.min.x
&& moved.max.x <= bounds.max.x
&& moved.min.y >= bounds.min.y
&& moved.max.y <= bounds.max.y
{
return moved;
}
}
frame
}
let entity = trigger.event().entity;
let Some((_, _, Some(Unmanaged::Floating))) = ctx.windows.get_managed(entity) else {
return;
};
debug!("Entity {entity} is floating.");
let parked_out_of_view = workspaces
.iter()
.any(|(strip, active)| !active && strip.contains(entity));
for (mut strip, _) in &mut workspaces {
if strip.contains(entity) {
strip.remove(entity);
}
}
let Some((display, dock)) = active_display.map(|display| *display) else {
return;
};
let display_bounds = display.actual_display_bounds(dock, &ctx.config);
let Some((window, frame)) = ctx.windows.get(entity).zip(ctx.windows.frame(entity)) else {
return;
};
let Some((_, app)) = ctx
.windows
.find_parent(window.id())
.and_then(|(_, _, parent)| apps.get(parent).ok())
else {
return;
};
let properties = WindowProperties::new(app, window, &ctx.config);
if parked_out_of_view {
debug!("Entity {entity} is floating on a hidden virtual row, keeping its frame.");
} else if let Some((rx, ry, rw, rh)) = properties.grid_ratios() {
let x = display_bounds.min.x + round_px(f64::from(display_bounds.width()) * rx);
let y = display_bounds.min.y + round_px(f64::from(display_bounds.height()) * ry);
let w = round_px(f64::from(display_bounds.width()) * rw);
let h = round_px(f64::from(display_bounds.height()) * rh);
ctx.commands.reposition_entity(entity, Origin::new(x, y));
ctx.commands.resize_entity(entity, Size::new(w, h));
} else if initializing.is_none() && !properties.floating() {
let max_width = display_bounds.width() * UNMANAGED_MAX_SCREEN_RATIO_NUM
/ UNMANAGED_MAX_SCREEN_RATIO_DEN;
let max_height = display_bounds.height() * UNMANAGED_MAX_SCREEN_RATIO_NUM
/ UNMANAGED_MAX_SCREEN_RATIO_DEN;
let new_width = frame.width().min(max_width);
let new_height = frame.height().min(max_height);
let mut target_frame =
IRect::from_corners(frame.min, frame.min + Origin::new(new_width, new_height));
target_frame = clamp_origin_to_bounds(target_frame, target_frame.size(), display_bounds);
target_frame =
offset_frame_within_bounds(target_frame, display_bounds, UNMANAGED_POP_OFFSET);
if target_frame.size() != frame.size() {
ctx.commands.resize_entity(
entity,
Size::new(target_frame.width(), target_frame.height()),
);
}
if target_frame.min != frame.min {
ctx.commands.reposition_entity(entity, target_frame.min);
}
}
}
fn remember_managed_strip(entity: Entity, strip: &LayoutStrip, commands: &mut Commands) {
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_insert(PreviousManagedStrip {
workspace_id: strip.id(),
virtual_index: strip.virtual_index,
index: strip.index_of(entity).unwrap_or(strip.len()),
});
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
pub(super) fn window_minimized_trigger(
trigger: On<Add, Unmanaged>,
windows: Windows,
workspaces: Query<(&mut LayoutStrip, Has<ActiveWorkspaceMarker>)>,
active_display: Single<&Display, With<ActiveDisplayMarker>>,
mut config: GlobalState,
mut commands: Commands,
) {
let entity = trigger.event().entity;
if let Some((_, _, Some(Unmanaged::Minimized | Unmanaged::Hidden))) =
windows.get_managed(entity)
{
debug!("Entity {entity} is minimized or hidden.");
let display_bounds = active_display.bounds();
for (mut strip, active) in workspaces {
if active {
give_away_focus(
entity,
&windows,
&strip,
&display_bounds,
&mut config,
&mut commands,
);
}
if strip.contains(entity) {
remember_managed_strip(entity, &strip, &mut commands);
strip.remove(entity);
}
}
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
pub(super) fn window_managed_trigger(
trigger: On<Remove, Unmanaged>,
active_display: Single<(&Display, Option<&DockPosition>), With<ActiveDisplayMarker>>,
apps: Query<(Entity, &Application)>,
mut workspaces: Query<
(
Entity,
&mut LayoutStrip,
&mut Position,
Has<ActiveWorkspaceMarker>,
),
Without<Window>,
>,
previous_strips: Query<&PreviousManagedStrip>,
initializing: Option<Res<Initializing>>,
mut ctx: WindowCtx,
) {
if initializing.is_some() {
return;
}
let entity = trigger.event().entity;
if ctx
.windows
.get(entity)
.is_some_and(|window| window.role().is_err())
{
return;
}
debug!("Entity {entity} is managed again.");
let (display, dock) = *active_display;
let display_bounds = display.actual_display_bounds(dock, &ctx.config);
let mut insert_at = previous_strips
.get(entity)
.ok()
.map(|previous| previous.index);
if let Some(window) = ctx.windows.get(entity)
&& let Some((_, app)) = ctx
.windows
.find_parent(window.id())
.and_then(|(_, _, parent)| apps.get(parent).ok())
{
let properties = WindowProperties::new(app, window, &ctx.config);
if let Some(width_ratio) = properties.fixed_width_ratio() {
let (_, pad_right, _, pad_left) = ctx.config.edge_padding();
let padded_width = display_bounds.width() - pad_left - pad_right;
let width = round_px(f64::from(padded_width) * width_ratio);
let height = display_bounds.height();
ctx.commands.resize_entity(entity, Size::new(width, height));
}
insert_at = properties.insertion().or(insert_at);
}
let previous = previous_strips.get(entity).ok().copied();
for (_, mut strip, _, _) in &mut workspaces {
strip.remove(entity);
}
let mut landed_in = None;
if let Some(previous) = previous {
for (strip_entity, mut strip, _, active) in &mut workspaces {
if strip.id() == previous.workspace_id && strip.virtual_index == previous.virtual_index
{
strip.insert_at(insert_at.unwrap_or(previous.index), entity);
landed_in = Some((strip_entity, active));
break;
}
}
}
if landed_in.is_none()
&& let Some((strip_entity, mut active_strip, _, _)) =
workspaces.iter_mut().find(|(_, _, _, active)| *active)
{
landed_in = Some((strip_entity, true));
if let Some(index) = insert_at {
active_strip.insert_at(index, entity);
} else {
let insertion = ctx.windows.frame(entity).and_then(|frame| {
let center_x = frame.center().x;
active_strip.all_columns().into_iter().position(|top| {
ctx.windows
.frame(top)
.is_some_and(|col| col.center().x > center_x)
})
});
let insertion = insertion.unwrap_or(active_strip.len());
active_strip.insert_at(insertion, entity);
}
}
if let Ok(mut entity_commands) = ctx.commands.get_entity(entity) {
entity_commands.try_remove::<PreviousManagedStrip>();
}
if let Some((strip_entity, false)) = landed_in {
if let Ok((_, _, mut position, _)) = workspaces.get_mut(strip_entity) {
position.set_changed();
}
return;
}
if let Some(origin) = ctx.windows.origin(entity) {
ctx.commands.reposition_entity(entity, origin);
}
if let Ok(mut entity_commands) = ctx.commands.get_entity(entity) {
entity_commands.try_insert(VerifyWindowPosition::default());
}
ctx.commands.reshuffle_around(entity);
}
#[instrument(level = Level::DEBUG, skip_all)]
pub(super) fn window_destroyed_trigger(
mut messages: MessageReader<Event>,
active_display: ActiveDisplay,
mut apps: Query<&mut Application>,
mut global_state: GlobalState,
mut focus_history: ResMut<FocusHistory>,
windows: Windows,
mut commands: Commands,
) {
for event in messages.read() {
let Event::WindowDestroyed { window_id, source } = event else {
continue;
};
let Some((window, entity, parent)) = windows.find_parent(*window_id) else {
debug!("Duplicate event: window {window_id} already destroyed.");
continue;
};
if matches!(source, DestroySource::SpaceNotification) && window.role().is_ok() {
debug!(
"Window {} still present, this was SLS workspace change.",
window.id()
);
continue;
}
let Ok(mut app) = apps.get_mut(parent) else {
error!("Window {} has no parent!", window.id());
continue;
};
app.unobserve_window(window);
give_away_focus(
entity,
&windows,
active_display.active_strip(),
&active_display.bounds(),
&mut global_state,
&mut commands,
);
focus_history.forget(entity);
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
}
}
pub(super) fn invalidate_window_title(mut messages: MessageReader<Event>, windows: Windows) {
for event in messages.read() {
let Event::WindowTitleChanged { window_id } = event else {
continue;
};
if let Some((window, _)) = windows.find(*window_id) {
window.invalidate_title();
}
}
}
fn give_away_focus(
entity: Entity,
windows: &Windows,
active_strip: &LayoutStrip,
viewport: &IRect,
config: &mut GlobalState,
commands: &mut Commands,
) {
if active_strip.is_tabbed_display(entity)
&& let Some(sibling) = active_strip.tab_display_sibling(entity)
{
config.set_ffm_flag(None);
commands.focus_entity(sibling, true);
return;
}
if active_strip.tabbed(entity) {
return;
}
let display_center = viewport.center().x;
let closest = active_strip
.all_columns()
.into_iter()
.filter(|&candidate| candidate != entity)
.filter_map(|candidate| {
let center = windows.moving_frame(candidate)?.center().x;
let distance = (center - display_center).abs();
Some((candidate, distance))
})
.min_by_key(|(_, dist)| *dist)
.map(|(e, _)| e)
.or_else(|| {
active_strip
.all_columns()
.into_iter()
.find(|&candidate| candidate != entity)
});
if let Some(neighbour) = closest
&& windows.get(neighbour).is_some()
{
config.set_ffm_flag(None);
commands.focus_entity(neighbour, true);
}
}
#[instrument(level = Level::DEBUG, skip_all)]
pub(super) fn spawn_window_trigger(
mut trigger: On<SpawnWindowTrigger>,
windows: Query<&Window>,
mut apps: Query<(Entity, &mut Application)>,
active_display: ActiveDisplay,
initializing: Option<Res<Initializing>>,
restore: Option<Res<crate::ecs::restore::SessionRestore>>,
mut commands: Commands,
) {
let new_windows = &mut trigger.event_mut().0;
while let Some(mut window) = new_windows.pop() {
let window_id = window.id();
if windows.iter().any(|window| window.id() == window_id) {
continue;
}
let Ok(pid) = window.pid() else {
trace!("Unable to get window pid for {window_id}");
continue;
};
let Some((app_entity, mut app)) = apps.iter_mut().find(|(_, app)| app.pid() == pid) else {
trace!("unable to find application with pid {pid}.");
continue;
};
if tracing::enabled!(Level::DEBUG) {
let title = window.title().unwrap_or_default();
let role = window.role().unwrap_or_default();
let subrole = window.subrole().unwrap_or_default();
let element = window
.element()
.map(|element| format!("{element}"))
.unwrap_or_default();
debug!(
"created {window_id} title: {title} role: {role} subrole: {subrole} element: {element}",
);
}
if app.observe_window(&window).is_err() {
warn!("Error observing window {window_id}.");
}
let Ok(frame) = window.update_frame().inspect_err(|err| error!("{err}")) else {
continue;
};
let position = Position(frame.min);
let bounds = Bounds(frame.size());
let width_ratio =
WidthRatio(f64::from(frame.width()) / f64::from(active_display.bounds().width()));
let layout_position = LayoutPosition::default();
let title = window.title().unwrap_or_default();
let app_name = app.name().to_string();
let bundle_id = app.bundle_id().unwrap_or_default().clone();
let window_frame = crate::types::state::Frame {
x: frame.min.x,
y: frame.min.y,
width: frame.width(),
height: frame.height(),
};
commands.spawn((
position,
bounds,
width_ratio,
window,
layout_position,
ChildOf(app_entity),
));
commands.trigger(SendMessageTrigger(Event::WindowSpawned {
window_id,
pid,
app_name,
bundle_id,
title,
frame: window_frame,
floating: false,
managed: true,
}));
}
if initializing.is_none() && restore.is_some() {
commands.trigger(RestoreWindowState);
}
}
pub(super) fn apply_window_defaults(
mut windows: WindowDefaultsQueries,
apps: Query<(Entity, &Application)>,
active_display: ActiveDisplay,
config: Res<Config>,
initializing: Option<Res<Initializing>>,
) {
let focused_ratio = windows.p1().single().ok().map(|ratio| ratio.0);
for (ref mut window, mut position, mut bounds, mut width_ratio, child) in windows.p0() {
let Ok((_, app)) = apps.get(child.parent()) else {
continue;
};
let properties = WindowProperties::new(app, window, &config);
debug!("Applying window defaults for '{}'", window.id());
let initializing = initializing.is_some();
if properties.floating() {
if !initializing && let Some((rx, ry, rw, rh)) = properties.grid_ratios() {
let bounds = active_display.actual_bounds(&config);
let x = bounds.min.x + round_px(f64::from(bounds.width()) * rx);
let y = bounds.min.y + round_px(f64::from(bounds.height()) * ry);
let w = round_px(f64::from(bounds.width()) * rw);
let h = round_px(f64::from(bounds.height()) * rh);
window.reposition(Origin::new(x, y));
window.resize(Size::new(w, h));
}
continue;
}
let vpadding = properties.vertical_padding();
let hpadding = properties.horizontal_padding();
window.set_padding(WindowPadding::Vertical(vpadding.clamp(0, 50)));
window.set_padding(WindowPadding::Horizontal(hpadding.clamp(0, 50)));
let display_width = f64::from(active_display.bounds().width());
if let Ok(frame) = window.update_frame() {
position.0 = frame.min;
bounds.bypass_change_detection().0 = frame.size();
width_ratio.0 = f64::from(frame.width()) / display_width;
}
if let Some(width) = properties.width_ratio(focused_ratio) {
let new_width = round_px(display_width * width);
let height = window.frame().height();
window.resize(Size::new(new_width, height));
if let Ok(frame) = window.update_frame().inspect_err(|err| error!("{err}")) {
position.0 = frame.min;
bounds.bypass_change_detection().0 = frame.size();
width_ratio.0 = f64::from(frame.width()) / display_width;
}
}
}
}
#[instrument(level = Level::DEBUG, skip_all)]
pub(super) fn apply_window_positions(
added: Populated<Entity, Added<Window>>,
mut workspaces: Query<(&mut LayoutStrip, Has<ActiveWorkspaceMarker>)>,
apps: Query<&Application>,
initializing: Option<Res<Initializing>>,
restore: Option<Res<crate::ecs::restore::SessionRestore>>,
restoration: Option<Res<PaneruState>>,
mut ctx: WindowCtx,
) {
for entity in added {
if workspaces.iter().any(|(strip, _)| strip.tabbed(entity)) {
debug!("Ignoring tabbed {entity} attributes.");
continue;
}
let Some((window, _, parent)) = ctx
.windows
.get(entity)
.and_then(|window| ctx.windows.find_parent(window.id()))
else {
continue;
};
let Ok(app) = apps.get(parent) else {
continue;
};
if crate::ecs::restore::matches_startup_restore_state(
window,
app,
restore.as_deref(),
restoration.as_deref(),
&ctx.config,
) {
continue;
}
let properties = WindowProperties::new(app, window, &ctx.config);
if properties.floating() {
if let Some(mut strip) = workspaces
.iter_mut()
.find_map(|(strip, _)| strip.contains(entity).then_some(strip))
{
strip.remove(entity);
}
if let Ok(mut entity_commands) = ctx.commands.get_entity(entity) {
entity_commands.try_insert(Unmanaged::Floating);
}
continue;
}
let allready_inserted = workspaces
.iter_mut()
.find_map(|(strip, _)| strip.contains(entity).then_some(strip));
if initializing.is_none()
&& allready_inserted.is_none()
&& let Some(mut strip) = workspaces
.iter_mut()
.find_map(|(strip, active)| active.then_some(strip))
{
let insert_at = properties.insertion().map_or_else(
|| {
let focused_window = ctx.windows.focused();
focused_window
.and_then(|(_, entity)| strip.index_of(entity).ok())
.and_then(|insert_at| {
(insert_at + 1 < strip.len()).then_some(insert_at + 1)
})
},
Some,
);
debug!("New window {entity} adding at {}", *strip);
match insert_at {
Some(after) => {
debug!("New window inserted at {after}");
strip.insert_at(after, entity);
}
None => strip.append(entity),
}
}
if initializing.is_none() {
if properties.dont_focus() {
if let Some((focus, prev)) = ctx.windows.focused() {
debug!(
"Not focusing new window {entity}, keeping focus on '{}'",
focus.title().unwrap_or_default()
);
ctx.commands.focus_entity(prev, true);
}
} else {
debug!("Synthesizing WindowFocused for newly spawned window {entity}");
ctx.commands
.trigger(SendMessageTrigger(Event::WindowFocused {
window_id: window.id(),
}));
}
}
}
}
pub(super) fn refresh_configuration_trigger(
mut messages: MessageReader<Event>,
window_manager: Res<WindowManager>,
mut config: ResMut<Config>,
mut watcher: Option<NonSendMut<Box<dyn Watcher>>>,
windows: Windows,
mut displays: Query<&mut Display>,
applications: Query<&Application>,
) {
for event in messages.read() {
let Event::ConfigRefresh(event) = event else {
continue;
};
let Some(ref mut watcher) = watcher else {
continue;
};
match &event.kind {
EventKind::Modify(
ModifyKind::Metadata(MetadataKind::WriteTime)
| ModifyKind::Data(DataChange::Content),
) => (),
EventKind::Remove(_) => {
for path in &event.paths {
_ = watcher.unwatch(path).inspect_err(|err| {
error!("unwatching the config '{}': {err}", path.display());
});
}
continue;
}
_ => continue,
}
if crate::config::CONFIGURATION_FILE.is_none() {
continue;
}
for path in &event.paths {
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("lua"))
{
continue;
}
if let Some(symlink) = symlink_target(path) {
debug!(
"symlink '{}' changed, replacing the watcher.",
symlink.display()
);
if let Some(new_watcher) =
crate::ecs::rewatch_configs(&window_manager, path.as_path())
{
**watcher = new_watcher;
}
}
info!("Reloading configuration file; {}", path.display());
_ = config.reload_config(path.as_path()).inspect_err(|err| {
error!("loading config '{}': {err}", path.display());
});
}
apply_config_side_effects(&config, &mut displays, &windows, &applications);
}
}
pub(super) fn window_removal_trigger(
trigger: On<Remove, Window>,
mut workspaces: Query<&mut LayoutStrip>,
) {
let entity = trigger.event().entity;
if let Some(mut strip) = workspaces.iter_mut().find(|strip| strip.contains(entity)) {
debug!(
"Removing despawned entity {entity} from strip {}",
strip.id()
);
strip.remove(entity);
}
}
pub(super) fn send_message_trigger(
trigger: On<SendMessageTrigger>,
mut messages: MessageWriter<Event>,
) {
let event = &trigger.event().0;
messages.write(event.clone());
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
pub(super) fn cleanup_timeout_trigger(
trigger: On<Remove, Timeout>,
all_timeouts: Query<&Timeout>,
mut commands: Commands,
) {
if let Ok(timeout) = all_timeouts.get(trigger.entity)
&& let Some(system_id) = timeout.system_id
{
commands.unregister_system(system_id);
}
}
pub(super) fn window_resize_verifier(
mut removed: RemovedComponents<ResizeMarker>,
mut windows: Query<(&mut Window, &Position, &mut Bounds)>,
layout_strips: Query<&LayoutStrip>,
mut commands: Commands,
) {
use std::cmp::Ordering;
for entity in removed.read() {
let Ok((mut window, _, mut bounds)) = windows.get_mut(entity) else {
continue;
};
let Ok(frame) = window.update_frame() else {
continue;
};
let actual_size = frame.size();
let expected_size = bounds.0;
let width_ord = fuzzy_equal(actual_size.x, expected_size.x);
let height_ord = fuzzy_equal(actual_size.y, expected_size.y);
if width_ord == Ordering::Equal && height_ord == Ordering::Equal {
continue;
}
debug!(
"window '{}'({}) did not fully resized to {}, was {} instead",
window.title().unwrap_or_default(),
window.id(),
expected_size,
actual_size,
);
bounds.0 = actual_size;
if width_ord == Ordering::Less {
let Some(column) = layout_strips.iter().find_map(|strip| {
strip
.index_of(entity)
.ok()
.and_then(|idx| strip.get(idx).ok())
}) else {
continue;
};
let get_window_frame = |entity| {
windows
.get(entity)
.map(|(_, position, bounds)| {
IRect::from_corners(position.0, position.0 + bounds.0)
})
.ok()
};
let Some(column_width) = column.width(&get_window_frame) else {
continue;
};
column
.window_iter()
.filter(|e| *e != entity)
.for_each(|entity| {
if let Some(width) = get_window_frame(entity).as_ref().map(IRect::width)
&& width < column_width
{
commands.resize_entity(entity, Size::new(column_width, actual_size.y));
}
});
}
}
}
fn fuzzy_equal<N>(actual_size: N, expected_size: N) -> Ordering
where
N: std::ops::Sub<Output = N> + Ord + From<i8>,
{
let diff = expected_size - actual_size;
if diff < N::from(-1) {
Ordering::Less
} else if diff > N::from(1) {
Ordering::Greater
} else {
Ordering::Equal
}
}