use accessibility_sys::{
AXUIElementRef, AXValueCreate, AXValueGetValue, kAXFloatingWindowSubrole, kAXPositionAttribute,
kAXRaiseAction, kAXSizeAttribute, kAXStandardWindowSubrole, kAXUnknownSubrole,
kAXValueTypeCGPoint, kAXValueTypeCGSize, kAXWindowRole,
};
use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::ecs::system::Commands;
use core::ptr::NonNull;
use derive_more::{DerefMut, with_trait::Deref};
use log::{debug, trace};
use objc2_core_foundation::{CFEqual, CFRetained, CFString, CFType, CGPoint, CGRect, CGSize};
use std::ptr::null_mut;
use std::thread;
use std::time::Duration;
use stdext::function_name;
use super::skylight::{
_AXUIElementGetWindow, _SLPSSetFrontProcessWithOptions, AXUIElementCopyAttributeValue,
AXUIElementPerformAction, AXUIElementSetAttributeValue, SLPSPostEventRecordTo,
};
use crate::ecs::params::ActiveDisplay;
use crate::ecs::{RepositionMarker, ResizeMarker, reposition_entity};
use crate::errors::{Error, Result};
use crate::platform::{Pid, ProcessSerialNumber, WinID};
use crate::util::{AXUIAttributes, AXUIWrapper, MacResult};
pub enum WindowPadding {
Vertical(u16),
Horizontal(u16),
}
pub trait WindowApi: Send + Sync {
fn id(&self) -> WinID;
fn psn(&self) -> Option<ProcessSerialNumber>;
fn frame(&self) -> CGRect;
fn element(&self) -> CFRetained<AXUIWrapper>;
fn title(&self) -> Result<String>;
fn child_role(&self) -> Result<bool>;
fn role(&self) -> Result<String>;
fn subrole(&self) -> Result<String>;
fn is_root(&self) -> bool;
fn is_eligible(&self) -> bool;
fn reposition(&mut self, x: f64, y: f64, display_bounds: &CGRect);
fn resize(&mut self, width: f64, height: f64, display_bounds: &CGRect);
fn update_frame(&mut self, display_bounds: &CGRect) -> Result<()>;
fn focus_without_raise(&self, currently_focused: &Window);
fn focus_with_raise(&self);
fn fully_visible(&self, display_bounds: &CGRect) -> bool {
self.frame().origin.x > 0.0
&& self.frame().origin.x < display_bounds.size.width - self.frame().size.width
}
fn expose_window(
&self,
active_display: &ActiveDisplay,
moving: Option<&RepositionMarker>,
resizing: Option<&ResizeMarker>,
entity: Entity,
commands: &mut Commands,
) -> CGRect {
let window_id = self.id();
let (mut origin, display_bounds) =
moving.map_or((self.frame().origin, active_display.bounds()), |marker| {
(
marker.origin,
active_display
.other()
.find(|display| display.id() == marker.display_id)
.map_or(active_display.bounds(), |display| display.bounds),
)
});
let size = resizing.map_or(self.frame().size, |marker| marker.size);
let moved = if origin.x + size.width > display_bounds.size.width {
trace!(
"{}: Bumped window {} to the left",
function_name!(),
window_id
);
origin.x = display_bounds.size.width - size.width;
true
} else if origin.x < 0.0 {
trace!(
"{}: Bumped window {} to the right",
function_name!(),
window_id
);
origin.x = 0.0;
true
} else {
false
};
if moved {
let display_id = moving.map_or(active_display.id(), |marker| marker.display_id);
reposition_entity(entity, origin.x, origin.y, display_id, commands);
trace!("{}: focus resposition to {origin:?}", function_name!());
}
CGRect::new(origin, size)
}
fn width_ratio(&self) -> f64;
fn pid(&self) -> Result<Pid>;
fn set_psn(&mut self, psn: ProcessSerialNumber);
fn set_eligible(&mut self, eligible: bool);
fn set_padding(&mut self, padding: WindowPadding);
}
#[derive(Component, Deref, DerefMut)]
pub struct Window(Box<dyn WindowApi>);
impl Window {
pub fn new(window: Box<dyn WindowApi>) -> Self {
Window(window)
}
}
pub fn ax_window_id(element_ref: AXUIElementRef) -> Result<WinID> {
let ptr = NonNull::new(element_ref).ok_or(Error::InvalidInput(format!(
"{}: nullptr passed as element.",
function_name!()
)))?;
let mut window_id: WinID = 0;
unsafe { _AXUIElementGetWindow(ptr.as_ptr(), &mut window_id) }.to_result(function_name!())?;
if window_id == 0 {
return Err(Error::InvalidInput(format!(
"{}: Unable to get window id from element {element_ref:?}.",
function_name!()
)));
}
Ok(window_id)
}
const CPS_USER_GENERATED: u32 = 0x200;
#[derive(Debug)]
pub struct WindowOS {
id: WinID,
psn: Option<ProcessSerialNumber>,
ax_element: CFRetained<AXUIWrapper>,
frame: CGRect,
vertical_padding: f64,
horizontal_padding: f64,
minimized: bool,
eligible: bool,
width_ratio: f64,
}
impl WindowOS {
pub fn new(element: &CFRetained<AXUIWrapper>) -> Result<Self> {
let id = ax_window_id(element.as_ptr())?;
let mut window = Self {
id,
psn: None,
ax_element: element.clone(),
frame: CGRect::default(),
vertical_padding: 0.0,
horizontal_padding: 0.0,
minimized: false,
eligible: false,
width_ratio: 0.33,
};
if window.is_unknown() {
return Err(Error::invalid_window(&format!(
"{}: Ignoring AXUnknown window, id: {}",
function_name!(),
window.id()
)));
}
if !window.is_real() {
return Err(Error::invalid_window(&format!(
"{}: Ignoring non-real window, id: {}",
function_name!(),
window.id()
)));
}
window.minimized = window.is_minimized();
trace!(
"{}: created {} title: {} role: {} subrole: {}",
function_name!(),
window.id(),
window.title().unwrap_or_default(),
window.role().unwrap_or_default(),
window.subrole().unwrap_or_default(),
);
Ok(window)
}
fn is_unknown(&self) -> bool {
self.subrole()
.is_ok_and(|subrole| subrole.eq(kAXUnknownSubrole))
}
fn is_minimized(&self) -> bool {
self.ax_element.minimized().is_ok_and(|minimized| minimized)
}
fn is_real(&self) -> bool {
let role = self.role().ok();
let subrole = self.subrole().ok();
subrole.as_deref() == Some(kAXStandardWindowSubrole)
|| (role.as_deref() == Some(kAXWindowRole)
&& subrole.as_deref() == Some(kAXFloatingWindowSubrole))
}
fn make_key_window(&self, psn: &ProcessSerialNumber) {
let window_id = self.id();
let mut event_bytes = [0u8; 0xf8];
event_bytes[0x04] = 0xf8;
event_bytes[0x3a] = 0x10;
event_bytes[0x3c..0x40].copy_from_slice(&window_id.to_ne_bytes());
event_bytes[0x20..0x30].fill(0xff);
event_bytes[0x08] = 0x01;
unsafe { SLPSPostEventRecordTo(psn, event_bytes.as_ptr().cast()) };
event_bytes[0x08] = 0x02;
unsafe { SLPSPostEventRecordTo(psn, event_bytes.as_ptr().cast()) };
}
}
impl WindowApi for WindowOS {
fn id(&self) -> WinID {
self.id
}
fn psn(&self) -> Option<ProcessSerialNumber> {
self.psn
}
fn frame(&self) -> CGRect {
self.frame
}
fn element(&self) -> CFRetained<AXUIWrapper> {
self.ax_element.clone()
}
fn title(&self) -> Result<String> {
self.ax_element.title()
}
fn child_role(&self) -> Result<bool> {
let role = self.role()?;
Ok(["AXSheet", "AXDrawer"]
.iter()
.any(|axrole| axrole.eq(&role)))
}
fn role(&self) -> Result<String> {
self.ax_element.role()
}
fn subrole(&self) -> Result<String> {
self.ax_element.subrole()
}
fn is_root(&self) -> bool {
let cftype = self.ax_element.as_ref();
self.ax_element
.parent()
.is_ok_and(|parent| !CFEqual(Some(&*parent), Some(cftype)))
}
fn is_eligible(&self) -> bool {
self.eligible
}
fn reposition(&mut self, x: f64, y: f64, display_bounds: &CGRect) {
if (self.frame.origin.x - x).abs() < 0.1 && (self.frame.origin.y - y).abs() < 0.1 {
trace!("{}: already in position.", function_name!());
return;
}
let mut point = CGPoint::new(
x + display_bounds.origin.x + self.horizontal_padding,
y + display_bounds.origin.y + self.vertical_padding,
);
let position_ref = unsafe {
AXValueCreate(
kAXValueTypeCGPoint,
NonNull::from(&mut point).as_ptr().cast(),
)
};
if let Ok(position) = AXUIWrapper::retain(position_ref) {
unsafe {
AXUIElementSetAttributeValue(
self.ax_element.as_ptr(),
CFString::from_static_str(kAXPositionAttribute).as_ref(),
position.as_ref(),
)
};
self.frame.origin.x = x;
self.frame.origin.y = y;
}
}
fn resize(&mut self, width: f64, height: f64, display_bounds: &CGRect) {
if (self.frame.size.width - width).abs() < 0.1
&& (self.frame.size.height - height).abs() < 0.1
{
trace!("{}: already correct size.", function_name!());
return;
}
let width_padding = 2.0 * self.horizontal_padding;
let height_padding = 2.0 * self.vertical_padding;
let mut size = CGSize::new(width - width_padding, height - height_padding);
let size_ref =
unsafe { AXValueCreate(kAXValueTypeCGSize, NonNull::from(&mut size).as_ptr().cast()) };
if let Ok(position) = AXUIWrapper::retain(size_ref) {
unsafe {
AXUIElementSetAttributeValue(
self.ax_element.as_ptr(),
CFString::from_static_str(kAXSizeAttribute).as_ref(),
position.as_ref(),
)
};
size.width += width_padding;
size.height += height_padding;
self.frame.size = size;
self.width_ratio = size.width / display_bounds.size.width;
}
}
fn update_frame(&mut self, display_bounds: &CGRect) -> Result<()> {
let window_ref = self.ax_element.as_ptr();
let position = unsafe {
let mut position_ref: *mut CFType = null_mut();
AXUIElementCopyAttributeValue(
window_ref,
CFString::from_static_str(kAXPositionAttribute).as_ref(),
&mut position_ref,
)
.to_result(function_name!())?;
AXUIWrapper::retain(position_ref)?
};
let size = unsafe {
let mut size_ref: *mut CFType = null_mut();
AXUIElementCopyAttributeValue(
window_ref,
CFString::from_static_str(kAXSizeAttribute).as_ref(),
&mut size_ref,
)
.to_result(function_name!())?;
AXUIWrapper::retain(size_ref)?
};
let mut frame = CGRect::default();
unsafe {
AXValueGetValue(
position.as_ptr(),
kAXValueTypeCGPoint,
NonNull::from(&mut frame.origin).as_ptr().cast(),
);
AXValueGetValue(
size.as_ptr(),
kAXValueTypeCGSize,
NonNull::from(&mut frame.size).as_ptr().cast(),
);
}
frame.origin.x -= display_bounds.origin.x;
frame.origin.y -= display_bounds.origin.y;
frame.size.width += 2.0 * self.horizontal_padding;
frame.size.height += 2.0 * self.vertical_padding;
frame.origin.x -= self.horizontal_padding;
frame.origin.y -= self.vertical_padding;
self.frame = frame;
self.width_ratio = frame.size.width / display_bounds.size.width;
Ok(())
}
fn focus_without_raise(&self, currently_focused: &Window) {
let Some((psn, focused_psn)) = self.psn().zip(currently_focused.psn()) else {
return;
};
let window_id = self.id();
debug!("{}: {window_id}", function_name!());
if focused_psn == psn {
let mut event_bytes = [0u8; 0xf8];
event_bytes[0x04] = 0xf8;
event_bytes[0x08] = 0x0d;
event_bytes[0x8a] = 0x02;
event_bytes[0x3c..0x40].copy_from_slice(¤tly_focused.id().to_ne_bytes());
unsafe {
SLPSPostEventRecordTo(&focused_psn, event_bytes.as_ptr().cast());
}
thread::sleep(Duration::from_millis(20));
event_bytes[0x8a] = 0x01;
event_bytes[0x3c..0x40].copy_from_slice(&window_id.to_ne_bytes());
unsafe {
SLPSPostEventRecordTo(&psn, event_bytes.as_ptr().cast());
}
}
unsafe {
_SLPSSetFrontProcessWithOptions(&psn, window_id, CPS_USER_GENERATED);
}
self.make_key_window(&psn);
}
fn focus_with_raise(&self) {
let Some(psn) = self.psn else {
return;
};
let window_id = self.id();
unsafe {
_SLPSSetFrontProcessWithOptions(&psn, window_id, CPS_USER_GENERATED);
}
self.make_key_window(&psn);
let element_ref = self.ax_element.as_ptr();
let action = CFString::from_static_str(kAXRaiseAction);
unsafe { AXUIElementPerformAction(element_ref, &action) };
}
fn width_ratio(&self) -> f64 {
self.width_ratio
}
fn pid(&self) -> Result<Pid> {
let pid: Pid = unsafe {
NonNull::new_unchecked(self.ax_element.as_ptr::<Pid>())
.byte_add(0x10)
.read()
};
(pid != 0).then_some(pid).ok_or(Error::InvalidInput(format!(
"{}: can not get pid from {:?}.",
function_name!(),
self.ax_element
)))
}
fn set_psn(&mut self, psn: ProcessSerialNumber) {
self.psn = Some(psn);
}
fn set_eligible(&mut self, eligible: bool) {
self.eligible = eligible;
}
fn set_padding(&mut self, padding: WindowPadding) {
match padding {
WindowPadding::Vertical(padding) => self.vertical_padding = f64::from(padding),
WindowPadding::Horizontal(padding) => self.horizontal_padding = f64::from(padding),
}
}
}