use std::ptr::NonNull;
use std::sync::LazyLock;
use objc2_app_kit::{NSRunningApplication, NSWorkspace};
use objc2_application_services::{AXError, AXUIElement};
use objc2_core_foundation::{CFArray, CFDictionary, CFNumber, CFRetained, CFString, CFType};
use objc2_core_graphics::{
CGWindowLevelForKey, CGWindowLevelKey, CGWindowListCopyWindowInfo, CGWindowListOption,
kCGWindowAlpha, kCGWindowBounds, kCGWindowLayer, kCGWindowNumber, kCGWindowOwnerPID,
};
use super::foreground::foreground_app_from_running_application;
use crate::pointer::hit_test::{Window, hit_test};
use crate::{HookBackend as _, PointerContext, PointerTarget};
type Dictionary = CFDictionary<CFString, CFType>;
pub(crate) fn pointer_context_supported() -> bool {
true
}
fn dictionary(value: CFRetained<CFType>) -> Option<CFRetained<Dictionary>> {
let value = value.downcast::<CFDictionary>().ok()?;
Some(unsafe { CFRetained::cast_unchecked::<Dictionary>(value) })
}
fn number(info: &Dictionary, key: &CFString) -> Option<CFRetained<CFNumber>> {
info.get(key)?.downcast::<CFNumber>().ok()
}
fn window(info: CFRetained<CFType>) -> Option<(Window, f64)> {
let info = dictionary(info)?;
let bounds_key = unsafe { kCGWindowBounds };
let bounds = dictionary(info.get(bounds_key)?)?;
let layer = number(&info, unsafe { kCGWindowLayer })?.as_i32()?;
let alpha = number(&info, unsafe { kCGWindowAlpha })?.as_f64()?;
let target = if layer == CGWindowLevelForKey(CGWindowLevelKey::DesktopWindowLevelKey)
|| layer == CGWindowLevelForKey(CGWindowLevelKey::DesktopIconWindowLevelKey)
{
PointerTarget::Desktop
} else if layer == CGWindowLevelForKey(CGWindowLevelKey::NormalWindowLevelKey) {
let process_id = number(&info, unsafe { kCGWindowOwnerPID })?.as_i32()?;
let window_id = number(&info, unsafe { kCGWindowNumber })?.as_i64()?;
if process_id <= 0 || window_id <= 0 {
return None;
}
PointerTarget::Window {
process_id,
window_id: u64::try_from(window_id).ok()?,
}
} else {
PointerTarget::Unavailable
};
Some((
Window {
x: number(&bounds, &CFString::from_static_str("X"))?.as_f64()?,
y: number(&bounds, &CFString::from_static_str("Y"))?.as_f64()?,
width: number(&bounds, &CFString::from_static_str("Width"))?.as_f64()?,
height: number(&bounds, &CFString::from_static_str("Height"))?.as_f64()?,
target,
},
alpha,
))
}
pub(crate) fn pointer_context() -> Option<PointerContext> {
let point = super::Backend::cursor_position()?;
let windows = CGWindowListCopyWindowInfo(CGWindowListOption::OptionOnScreenOnly, 0)?;
let windows = unsafe { CFRetained::cast_unchecked::<CFArray<CFType>>(windows) };
let candidates = windows.into_iter().filter_map(|info| match window(info) {
Some((_, alpha)) if alpha <= 0.0 => None,
Some((window, _)) => Some(Some(window)),
None => Some(None),
});
let target = hit_test(point, candidates);
let app = if let PointerTarget::Window { process_id, .. } = target {
Some(objc2::rc::autoreleasepool(|pool| {
let app = NSRunningApplication::runningApplicationWithProcessIdentifier(process_id)?;
foreground_app_from_running_application(&app, pool)
})?)
} else {
None
};
Some(PointerContext { app, target })
}
type WindowIdFn = unsafe extern "C" fn(*const AXUIElement, *mut u32) -> AXError;
static WINDOW_ID: LazyLock<Option<WindowIdFn>> = LazyLock::new(|| {
let symbol = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"_AXUIElementGetWindow".as_ptr()) };
if symbol.is_null() {
return None;
}
Some(unsafe { std::mem::transmute::<*mut libc::c_void, WindowIdFn>(symbol) })
});
pub(crate) fn pointer_target_is_focused(target: PointerTarget) -> bool {
let PointerTarget::Window {
process_id,
window_id,
} = target
else {
return false;
};
focused_window_id(process_id) == Some(window_id)
}
fn focused_window_id(pid: i32) -> Option<u64> {
let get_window_id = (*WINDOW_ID)?;
objc2::rc::autoreleasepool(|_| {
let workspace = NSWorkspace::sharedWorkspace();
if workspace.frontmostApplication()?.processIdentifier() != pid {
return None;
}
let app = unsafe { AXUIElement::new_application(pid) };
if unsafe { app.set_messaging_timeout(0.1) } != AXError::Success {
return None;
}
let mut value = std::ptr::null();
let attr = CFString::from_static_str("AXFocusedWindow");
if unsafe { app.copy_attribute_value(&attr, NonNull::from(&mut value)) } != AXError::Success
{
return None;
}
let value = NonNull::new(value.cast_mut())?;
let value = unsafe { CFRetained::from_raw(value) };
let window = value.downcast::<AXUIElement>().ok()?;
if unsafe { window.set_messaging_timeout(0.1) } != AXError::Success {
return None;
}
let mut id = 0;
if unsafe { get_window_id(&raw const *window, &raw mut id) } != AXError::Success || id == 0
{
return None;
}
if workspace.frontmostApplication()?.processIdentifier() != pid {
return None;
}
Some(u64::from(id))
})
}