use bevy::app::{App as BevyApp, AppExit, Startup, Update};
use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::ecs::message::{Message, MessageReader, Messages};
use bevy::ecs::query::With;
use bevy::ecs::resource::Resource;
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::ecs::schedule::common_conditions::any_with_component;
use bevy::ecs::system::{Commands, Query, Res};
use bevy::ecs::world::World;
use bevy::prelude::Event as BevyEvent;
use bevy::time::{Time, Virtual};
use log::{debug, error, info, trace, warn};
use objc2::rc::Retained;
use objc2_core_foundation::{CFRetained, CGPoint, CGSize};
use objc2_core_graphics::CGDirectDisplayID;
use std::collections::HashMap;
use std::io::{Error, ErrorKind, Result};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
use std::thread;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use stdext::function_name;
use crate::app::Application;
use crate::commands::process_command_trigger;
use crate::config::Config;
use crate::manager::WindowManager;
use crate::platform::{ProcessSerialNumber, WorkspaceObserver};
use crate::process::{Process, ProcessRef};
use crate::skylight::{ConnID, SLSMainConnectionID, WinID};
use crate::util::AXUIWrapper;
use crate::windows::{Display, Window, WindowPane};
#[allow(dead_code)]
#[derive(Clone, Debug, Message)]
pub enum Event {
Exit,
ProcessesLoaded,
ConfigRefresh {
config: Config,
},
ApplicationLaunched {
psn: ProcessSerialNumber,
observer: Retained<WorkspaceObserver>,
},
ApplicationTerminated {
psn: ProcessSerialNumber,
},
ApplicationFrontSwitched {
psn: ProcessSerialNumber,
},
ApplicationActivated,
ApplicationDeactivated,
ApplicationVisible {
msg: String,
},
ApplicationHidden {
msg: String,
},
WindowCreated {
element: CFRetained<AXUIWrapper>,
},
WindowDestroyed {
window_id: WinID,
},
WindowFocused {
window_id: WinID,
},
WindowMoved {
window_id: WinID,
},
WindowResized {
window_id: WinID,
},
WindowMinimized {
window_id: WinID,
},
WindowDeminimized {
window_id: WinID,
},
WindowTitleChanged {
window_id: WinID,
},
CurrentlyFocused,
MouseDown {
point: CGPoint,
},
MouseUp {
point: CGPoint,
},
MouseDragged {
point: CGPoint,
},
MouseMoved {
point: CGPoint,
},
Swipe {
deltas: Vec<f64>,
},
SpaceCreated,
SpaceDestroyed,
SpaceChanged,
DisplayAdded {
display_id: CGDirectDisplayID,
},
DisplayRemoved {
display_id: CGDirectDisplayID,
},
DisplayMoved {
display_id: CGDirectDisplayID,
},
DisplayResized {
display_id: CGDirectDisplayID,
},
DisplayConfigured {
display_id: CGDirectDisplayID,
},
DisplayChanged,
MissionControlShowAllWindows,
MissionControlShowFrontWindows,
MissionControlShowDesktop,
MissionControlExit,
DockDidChangePref {
msg: String,
},
DockDidRestart {
msg: String,
},
MenuOpened {
window_id: WinID,
},
MenuClosed {
window_id: WinID,
},
MenuBarHiddenChanged {
msg: String,
},
SystemWoke {
msg: String,
},
Command {
argv: Vec<String>,
},
TypeCount,
}
#[derive(Clone, Debug)]
pub struct EventSender {
tx: Sender<Event>,
}
impl EventSender {
fn new() -> (Self, Receiver<Event>) {
let (tx, rx) = channel::<Event>();
(Self { tx }, rx)
}
pub fn send(&self, event: Event) -> Result<()> {
self.tx
.send(event)
.map_err(|err| {
Error::new(
ErrorKind::ConnectionAborted,
format!("{}: sending event: {err}", function_name!()),
)
})
.inspect_err(|err| error!("{err}"))
}
}
#[derive(Component)]
pub struct InitializingMarker;
#[derive(Component)]
pub struct FocusedMarker;
#[derive(Component)]
pub struct FreshMarker;
#[derive(Component)]
pub struct ExistingMarker;
#[derive(Component)]
pub struct RepositionMarker {
pub origin: CGPoint,
}
#[derive(Component)]
pub struct ResizeMarker {
pub size: CGSize,
}
#[derive(Component)]
pub struct BProcess(pub ProcessRef);
#[derive(Resource)]
pub struct MainConnection(pub ConnID);
#[derive(Resource)]
pub struct SenderSocket(pub EventSender);
#[derive(Resource)]
pub struct SkipReshuffle(pub bool);
#[derive(Resource)]
pub struct MissionControlActive(pub bool);
#[derive(Resource)]
pub struct FocusFollowsMouse(pub Option<WinID>);
#[derive(Resource)]
pub struct OrphanedSpaces(pub HashMap<u64, WindowPane>);
#[derive(BevyEvent)]
pub struct WMEventTrigger(pub Event);
#[derive(BevyEvent)]
pub struct CommandTrigger(pub Vec<String>);
#[derive(BevyEvent)]
pub struct ReshuffleAroundTrigger(pub WinID);
#[derive(BevyEvent)]
pub struct SpawnWindowTrigger(pub Vec<Window>);
pub struct EventHandler;
impl EventHandler {
pub fn run() -> (EventSender, Arc<AtomicBool>, JoinHandle<()>) {
let (sender, receiver) = EventSender::new();
let quit = Arc::new(AtomicBool::new(false));
(
sender.clone(),
quit.clone(),
thread::spawn(move || {
let main_cid = unsafe { SLSMainConnectionID() };
debug!("{}: My connection id: {main_cid}", function_name!());
let mut existing_processes =
EventHandler::gather_initial_processes(&receiver, &sender);
let process_setup = move |world: &mut World| {
EventHandler::initial_setup(world, &mut existing_processes);
};
BevyApp::new()
.set_runner(move |app| EventHandler::custom_loop(app, &receiver))
.init_resource::<Messages<Event>>()
.insert_resource(Time::<Virtual>::from_max_delta(Duration::from_secs(10)))
.insert_resource(MainConnection(main_cid))
.insert_resource(SenderSocket(sender))
.insert_resource(SkipReshuffle(false))
.insert_resource(MissionControlActive(false))
.insert_resource(FocusFollowsMouse(None))
.insert_resource(OrphanedSpaces(HashMap::new()))
.add_observer(process_command_trigger)
.add_observer(WindowManager::mouse_moved_trigger)
.add_observer(WindowManager::mouse_down_trigger)
.add_observer(WindowManager::mouse_dragged_trigger)
.add_observer(WindowManager::display_change_trigger)
.add_observer(WindowManager::display_add_trigger)
.add_observer(WindowManager::display_remove_trigger)
.add_observer(WindowManager::display_moved_trigger)
.add_observer(WindowManager::front_switched_trigger)
.add_observer(WindowManager::window_focused_trigger)
.add_observer(WindowManager::reshuffle_around_trigger)
.add_observer(WindowManager::swipe_gesture_trigger)
.add_observer(WindowManager::mission_control_trigger)
.add_observer(WindowManager::application_event_trigger)
.add_observer(WindowManager::dispatch_application_messages)
.add_observer(WindowManager::window_resized_trigger)
.add_observer(WindowManager::window_destroyed_trigger)
.add_observer(WindowManager::spawn_window_trigger)
.add_systems(Startup, EventHandler::gather_displays)
.add_systems(Startup, process_setup.after(EventHandler::gather_displays))
.add_systems(
Update,
(
EventHandler::dispatch_toplevel_triggers,
EventHandler::animate_windows,
EventHandler::animate_resize_windows,
WindowManager::add_existing_process
.run_if(any_with_component::<InitializingMarker>),
WindowManager::add_existing_application
.run_if(any_with_component::<InitializingMarker>),
EventHandler::finish_setup
.run_if(any_with_component::<InitializingMarker>),
WindowManager::add_launched_process,
WindowManager::add_launched_application,
),
)
.run();
quit.store(true, std::sync::atomic::Ordering::Relaxed);
}),
)
}
fn custom_loop(mut app: BevyApp, rx: &Receiver<Event>) -> AppExit {
const LOOP_MAX_TIMEOUT_MS: u64 = 5000;
const LOOP_TIMEOUT_STEP: u64 = 5;
app.finish();
app.cleanup();
let mut timeout = LOOP_TIMEOUT_STEP;
let mut last_update = Instant::now();
while app.should_exit().is_none() {
app.update();
match rx.recv_timeout(Duration::from_millis(timeout)) {
Ok(Event::Exit) => {
app.world_mut().write_message::<AppExit>(AppExit::Success);
}
Ok(event) => {
app.world_mut().write_message::<Event>(event);
timeout = LOOP_TIMEOUT_STEP;
}
Err(RecvTimeoutError::Timeout) => {
timeout = timeout.min(LOOP_MAX_TIMEOUT_MS) + LOOP_TIMEOUT_STEP;
}
_ => todo!(),
}
let now = Instant::now();
let delta = now - last_update;
last_update = now;
app.world_mut()
.resource_mut::<Time<Virtual>>()
.advance_by(delta);
}
AppExit::Success
}
#[allow(clippy::needless_pass_by_value)]
fn dispatch_toplevel_triggers(mut messages: MessageReader<Event>, mut commands: Commands) {
for event in messages.read() {
match event {
Event::Command { argv } => commands.trigger(CommandTrigger(argv.clone())),
Event::ConfigRefresh { config } => {
info!("{}: Configuration changed.", function_name!());
commands.insert_resource(config.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)]
fn gather_displays(cid: Res<MainConnection>, mut commands: Commands) {
let Ok(active_display) = Display::active_display_id(cid.0) else {
error!("{}: Unable to get active display id!", function_name!());
return;
};
for display in Display::present_displays(cid.0) {
if display.id == active_display {
commands.spawn((display, FocusedMarker));
} else {
commands.spawn(display);
}
}
}
fn gather_initial_processes(
receiver: &Receiver<Event>,
sender: &EventSender,
) -> Vec<ProcessRef> {
let mut out = Vec::new();
let mut initial_config = None;
loop {
match receiver.recv() {
Ok(Event::ProcessesLoaded | Event::Exit) => break,
Ok(Event::ApplicationLaunched { psn, observer }) => {
out.push(Process::new(&psn, observer.clone()));
}
Ok(Event::ConfigRefresh { config }) => {
initial_config = Some(config);
}
Ok(event) => warn!(
"{}: Stray event during initial process gathering: {event:?}",
function_name!()
),
Err(err) => {
warn!(
"{}: Error reading initial processes: {err}",
function_name!()
);
break;
}
}
}
if let Some(config) = initial_config {
_ = sender.send(Event::ConfigRefresh { config });
}
out
}
fn initial_setup(world: &mut World, existing_processes: &mut Vec<ProcessRef>) {
loop {
let Some(mut process) = existing_processes.pop() else {
break;
};
if !process.is_observable() {
debug!(
"{}: Existing application {} is not observable, ignoring it.",
function_name!(),
process.name,
);
continue;
}
debug!(
"{}: Adding existing process {}",
function_name!(),
process.name
);
world.spawn((ExistingMarker, BProcess(process)));
}
world.spawn(InitializingMarker);
}
#[allow(clippy::needless_pass_by_value)]
fn finish_setup(
apps: Query<(&Application, Entity, Option<&FreshMarker>)>,
mut windows: Query<(&mut Window, Entity)>,
initializing: Query<(Entity, &InitializingMarker)>,
displays: Query<(&mut Display, Option<&FocusedMarker>)>,
main_cid: Res<MainConnection>,
mut commands: Commands,
) {
if !windows.is_empty()
&& apps.iter().all(|(_, _, fresh)| fresh.is_none())
&& let Ok((entity, _)) = initializing.single()
{
commands.entity(entity).despawn();
info!(
"{}: Finished Initialization: found {} windows.",
function_name!(),
windows.iter().len()
);
for (mut display, active) in displays {
WindowManager::refresh_display(main_cid.0, &mut display, &mut windows);
if active.is_some() {
let first_window = display
.active_panel(main_cid.0)
.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).insert(FocusedMarker);
}
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn animate_windows(
windows: Query<(&mut Window, Entity, &RepositionMarker)>,
displays: Query<&Display, With<FocusedMarker>>,
time: Res<Time<Virtual>>,
config: Option<Res<Config>>,
mut commands: Commands,
) {
let Ok(active_display) = displays.single() else {
return;
};
let move_speed = config
.and_then(|config| 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 }) in windows {
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).remove::<RepositionMarker>();
window.reposition(
origin.x,
origin.y.max(active_display.menubar_height),
&active_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(active_display.menubar_height),
&active_display.bounds,
);
}
}
#[allow(clippy::needless_pass_by_value)]
fn animate_resize_windows(
windows: Query<(&mut Window, Entity, &ResizeMarker)>,
displays: Query<&Display, With<FocusedMarker>>,
mut commands: Commands,
) {
let Ok(active_display) = displays.single() else {
return;
};
for (mut window, entity, ResizeMarker { size }) in windows {
let origin = window.frame().origin;
let width = if origin.x + size.width < active_display.bounds.size.width + 0.4 {
commands.entity(entity).remove::<ResizeMarker>();
size.width
} else {
active_display.bounds.size.width - origin.x
};
debug!(
"{}: window {} resize {}:{}",
function_name!(),
window.id(),
width,
size.height,
);
window.resize(width, size.height, &active_display.bounds);
}
}
}