use std::ffi::c_void;
use image::RgbaImage;
use objc2_app_kit::NSWorkspace;
use objc2_core_foundation::CGSize;
use objc2_core_foundation::{
CFBoolean, CFDictionary, CFNumber, CFNumberType, CFRetained, CFString, CGPoint, CGRect,
};
use objc2_core_graphics::{
CGDisplayBounds, CGMainDisplayID, CGRectContainsPoint, CGRectIntersectsRect,
CGRectMakeWithDictionaryRepresentation, CGWindowListCopyWindowInfo, CGWindowListOption,
};
use objc2_foundation::{NSNumber, NSString};
use crate::{XCapError, error::XCapResult};
use super::{capture::capture, impl_monitor::ImplMonitor};
#[derive(Debug, Clone)]
pub(crate) struct ImplWindow {
pub window_id: u32,
pid_inner: Option<u32>,
app_name_inner: Option<String>,
title_inner: Option<String>,
x_inner: Option<i32>,
y_inner: Option<i32>,
width_inner: Option<u32>,
height_inner: Option<u32>,
is_on_screen_inner: Option<bool>,
z_inner: Option<i32>,
}
unsafe impl Send for ImplWindow {}
fn get_cf_dictionary_get_value(
cf_dictionary: &CFDictionary,
key: &str,
) -> XCapResult<*const c_void> {
unsafe {
let cf_dictionary_key = CFString::from_str(key);
let cf_dictionary_key_ref = cf_dictionary_key.as_ref() as *const CFString;
let value = cf_dictionary.value(cf_dictionary_key_ref.cast());
if value.is_null() {
return Err(XCapError::new(format!(
"Get CFDictionary {} value failed",
key
)));
}
Ok(value)
}
}
fn get_cf_number_i32_value(cf_dictionary: &CFDictionary, key: &str) -> XCapResult<i32> {
unsafe {
let cf_number = get_cf_dictionary_get_value(cf_dictionary, key)? as *const CFNumber;
let mut value: i32 = 0;
let is_success =
(*cf_number).value(CFNumberType::IntType, &mut value as *mut _ as *mut c_void);
if !is_success {
return Err(XCapError::new(format!(
"Get {} CFNumberGetValue failed",
key
)));
}
Ok(value)
}
}
fn get_cf_string_value(cf_dictionary: &CFDictionary, key: &str) -> XCapResult<String> {
let value_ref = get_cf_dictionary_get_value(cf_dictionary, key)? as *const CFString;
let value = unsafe { (*value_ref).to_string() };
Ok(value)
}
fn get_cf_bool_value(cf_dictionary: &CFDictionary, key: &str) -> XCapResult<bool> {
let value_ref = get_cf_dictionary_get_value(cf_dictionary, key)? as *const CFBoolean;
Ok(unsafe { (*value_ref).value() })
}
fn get_window_cg_rect(window_cf_dictionary: &CFDictionary) -> XCapResult<CGRect> {
unsafe {
let window_bounds = get_cf_dictionary_get_value(window_cf_dictionary, "kCGWindowBounds")?
as *const CFDictionary;
let mut cg_rect = CGRect::default();
let is_success =
CGRectMakeWithDictionaryRepresentation(Some(&*window_bounds), &mut cg_rect);
if !is_success {
return Err(XCapError::new(
"CGRectMakeWithDictionaryRepresentation failed",
));
}
Ok(cg_rect)
}
}
fn get_window_id(window_cf_dictionary: &CFDictionary) -> XCapResult<u32> {
let window_name = get_cf_string_value(window_cf_dictionary, "kCGWindowName")?;
let window_owner_name = get_cf_string_value(window_cf_dictionary, "kCGWindowOwnerName")?;
if window_name.eq("StatusIndicator") && window_owner_name.eq("Window Server") {
return Err(XCapError::new("Window is StatusIndicator"));
}
let window_sharing_state =
get_cf_number_i32_value(window_cf_dictionary, "kCGWindowSharingState")?;
if window_sharing_state == 0 {
return Err(XCapError::new("Window sharing state is 0"));
}
let window_id = get_cf_number_i32_value(window_cf_dictionary, "kCGWindowNumber")?;
Ok(window_id as u32)
}
pub fn get_window_cf_dictionary(window_id: u32) -> XCapResult<CFRetained<CFDictionary>> {
unsafe {
let cf_array = match CGWindowListCopyWindowInfo(
CGWindowListOption::OptionAll | CGWindowListOption::ExcludeDesktopElements,
0,
) {
Some(cf_array) => cf_array,
None => return Err(XCapError::new("Get window info failed")),
};
let windows_count = cf_array.count();
for i in 0..windows_count {
let window_cf_dictionary_ref = cf_array.value_at_index(i) as *const CFDictionary;
if window_cf_dictionary_ref.is_null() {
continue;
}
let window_cf_dictionary = &*window_cf_dictionary_ref;
let current_window_id = match get_window_id(window_cf_dictionary) {
Ok(val) => val,
Err(_) => continue,
};
if current_window_id == window_id {
let s = CFDictionary::new_copy(None, Some(window_cf_dictionary)).unwrap();
return Ok(s);
}
}
Err(XCapError::new("Window not found"))
}
}
impl ImplWindow {
fn from_cf_dictionary(window_id: u32, dict: &CFDictionary) -> ImplWindow {
let pid = get_cf_number_i32_value(dict, "kCGWindowOwnerPID")
.ok()
.map(|p| p as u32);
let app_name = get_cf_string_value(dict, "kCGWindowOwnerName").ok();
let title = get_cf_string_value(dict, "kCGWindowName").ok();
let cg_rect = get_window_cg_rect(dict).ok();
let x = cg_rect.map(|r| r.origin.x as i32);
let y = cg_rect.map(|r| r.origin.y as i32);
let width = cg_rect.map(|r| r.size.width as u32);
let height = cg_rect.map(|r| r.size.height as u32);
let is_on_screen = get_cf_bool_value(dict, "kCGWindowIsOnscreen").ok();
ImplWindow {
window_id,
pid_inner: pid,
app_name_inner: app_name,
title_inner: title,
x_inner: x,
y_inner: y,
width_inner: width,
height_inner: height,
is_on_screen_inner: is_on_screen,
z_inner: None, }
}
pub fn all() -> XCapResult<Vec<ImplWindow>> {
unsafe {
let mut impl_window = Vec::new();
let cf_array = match CGWindowListCopyWindowInfo(
CGWindowListOption::OptionOnScreenOnly | CGWindowListOption::ExcludeDesktopElements,
0,
) {
Some(cf_array) => cf_array,
None => return Ok(impl_window),
};
let windows_count = cf_array.count();
for i in 0..windows_count {
let window_cf_dictionary_ref = cf_array.value_at_index(i) as *const CFDictionary;
if window_cf_dictionary_ref.is_null() {
continue;
}
let window_cf_dictionary = &*window_cf_dictionary_ref;
let window_id = match get_window_id(window_cf_dictionary) {
Ok(window_id) => window_id,
Err(_) => continue,
};
impl_window.push(ImplWindow::from_cf_dictionary(
window_id,
window_cf_dictionary,
));
}
Ok(impl_window)
}
}
pub fn list_all() -> XCapResult<Vec<ImplWindow>> {
unsafe {
let mut impl_window = Vec::new();
let cf_array = match CGWindowListCopyWindowInfo(
CGWindowListOption::OptionAll | CGWindowListOption::ExcludeDesktopElements,
0,
) {
Some(cf_array) => cf_array,
None => return Ok(impl_window),
};
let windows_count = cf_array.count();
for i in 0..windows_count {
let window_cf_dictionary_ref = cf_array.value_at_index(i) as *const CFDictionary;
if window_cf_dictionary_ref.is_null() {
continue;
}
let window_cf_dictionary = &*window_cf_dictionary_ref;
let window_id = match get_window_id(window_cf_dictionary) {
Ok(window_id) => window_id,
Err(_) => continue,
};
impl_window.push(ImplWindow::from_cf_dictionary(
window_id,
window_cf_dictionary,
));
}
Ok(impl_window)
}
}
}
impl ImplWindow {
pub fn id(&self) -> XCapResult<u32> {
Ok(self.window_id)
}
pub fn pid(&self) -> XCapResult<u32> {
if let Some(pid) = self.pid_inner {
return Ok(pid);
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let pid = get_cf_number_i32_value(window_cf_dictionary.as_ref(), "kCGWindowOwnerPID")?;
Ok(pid as u32)
}
pub fn app_name(&self) -> XCapResult<String> {
if let Some(ref app_name) = self.app_name_inner {
return Ok(app_name.clone());
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
get_cf_string_value(window_cf_dictionary.as_ref(), "kCGWindowOwnerName")
}
pub fn title(&self) -> XCapResult<String> {
if let Some(ref title) = self.title_inner {
return Ok(title.clone());
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
get_cf_string_value(window_cf_dictionary.as_ref(), "kCGWindowName")
}
pub fn current_monitor(&self) -> XCapResult<ImplMonitor> {
let cg_rect = if let (Some(x), Some(y), Some(width), Some(height)) = (
self.x_inner,
self.y_inner,
self.width_inner,
self.height_inner,
) {
CGRect {
origin: CGPoint {
x: x as f64,
y: y as f64,
},
size: CGSize {
width: width as f64,
height: height as f64,
},
}
} else {
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
get_window_cg_rect(window_cf_dictionary.as_ref())?
};
let window_center_x = cg_rect.origin.x + cg_rect.size.width / 2.0;
let window_center_y = cg_rect.origin.y + cg_rect.size.height / 2.0;
let cg_point = CGPoint {
x: window_center_x,
y: window_center_y,
};
let impl_monitors = ImplMonitor::all()?;
let primary_monitor = ImplMonitor::new(unsafe { CGMainDisplayID() });
let impl_monitor = impl_monitors
.iter()
.find(|impl_monitor| unsafe {
let display_bounds = CGDisplayBounds(impl_monitor.cg_direct_display_id);
CGRectContainsPoint(display_bounds, cg_point)
|| CGRectIntersectsRect(display_bounds, cg_rect)
})
.unwrap_or(&primary_monitor);
Ok(impl_monitor.to_owned())
}
pub fn x(&self) -> XCapResult<i32> {
if let Some(x) = self.x_inner {
return Ok(x);
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let cg_rect = get_window_cg_rect(window_cf_dictionary.as_ref())?;
Ok(cg_rect.origin.x as i32)
}
pub fn y(&self) -> XCapResult<i32> {
if let Some(y) = self.y_inner {
return Ok(y);
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let cg_rect = get_window_cg_rect(window_cf_dictionary.as_ref())?;
Ok(cg_rect.origin.y as i32)
}
pub fn z(&self) -> XCapResult<i32> {
if let Some(z) = self.z_inner {
return Ok(z);
}
unsafe {
let cf_array = match CGWindowListCopyWindowInfo(
CGWindowListOption::OptionOnScreenOnly | CGWindowListOption::ExcludeDesktopElements,
0,
) {
Some(cf_array) => cf_array,
None => return Err(XCapError::new("Get window list failed")),
};
let windows_count = cf_array.count();
let mut z = windows_count as i32;
for i in 0..windows_count {
z -= 1;
let window_cf_dictionary_ref = cf_array.value_at_index(i) as *const CFDictionary;
if window_cf_dictionary_ref.is_null() {
continue;
}
let window_cf_dictionary = &*window_cf_dictionary_ref;
let window_id = match get_window_id(window_cf_dictionary) {
Ok(window_id) => window_id,
Err(_) => continue,
};
if window_id == self.window_id {
break;
}
}
Ok(z)
}
}
pub fn width(&self) -> XCapResult<u32> {
if let Some(width) = self.width_inner {
return Ok(width);
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let cg_rect = get_window_cg_rect(window_cf_dictionary.as_ref())?;
Ok(cg_rect.size.width as u32)
}
pub fn height(&self) -> XCapResult<u32> {
if let Some(height) = self.height_inner {
return Ok(height);
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let cg_rect = get_window_cg_rect(window_cf_dictionary.as_ref())?;
Ok(cg_rect.size.height as u32)
}
pub fn is_minimized(&self) -> XCapResult<bool> {
if let Some(is_on_screen) = self.is_on_screen_inner {
let is_maximized = self.is_maximized()?;
return Ok(!is_on_screen && !is_maximized);
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let is_on_screen = get_cf_bool_value(window_cf_dictionary.as_ref(), "kCGWindowIsOnscreen")
.unwrap_or(false); let is_maximized = self.is_maximized()?;
Ok(!is_on_screen && !is_maximized)
}
pub fn is_maximized(&self) -> XCapResult<bool> {
if let (Some(width), Some(height)) = (self.width_inner, self.height_inner) {
let impl_monitor = self.current_monitor()?;
let impl_monitor_width = impl_monitor.width()?;
let impl_monitor_height = impl_monitor.height()?;
let is_maximized = { width >= impl_monitor_width && height >= impl_monitor_height };
return Ok(is_maximized);
}
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let cg_rect = get_window_cg_rect(window_cf_dictionary.as_ref())?;
let impl_monitor = self.current_monitor()?;
let impl_monitor_width = impl_monitor.width()?;
let impl_monitor_height = impl_monitor.height()?;
let is_maximized = {
cg_rect.size.width as u32 >= impl_monitor_width
&& cg_rect.size.height as u32 >= impl_monitor_height
};
Ok(is_maximized)
}
pub fn is_focused(&self) -> XCapResult<bool> {
let pid_key = NSString::from_str("NSApplicationProcessIdentifier");
unsafe {
let workspace = NSWorkspace::sharedWorkspace();
let active_app_dictionary = workspace.activeApplication();
let active_app_pid = active_app_dictionary
.and_then(|dict| dict.valueForKey(&pid_key))
.and_then(|pid| pid.downcast::<NSNumber>().ok())
.map(|pid| pid.intValue() as u32);
if active_app_pid == self.pid().ok() {
return Ok(true);
}
Ok(false)
}
}
pub fn capture_image(&self) -> XCapResult<RgbaImage> {
let window_cf_dictionary = get_window_cf_dictionary(self.window_id)?;
let cg_rect = get_window_cg_rect(window_cf_dictionary.as_ref())?;
capture(
cg_rect,
CGWindowListOption::OptionIncludingWindow,
self.window_id,
)
}
pub fn capture_thumbnail(&self) -> XCapResult<RgbaImage> {
let original_image = self.capture_image()?;
let original_width = original_image.width() as f64;
let original_height = original_image.height() as f64;
let max_width = 200.0;
let scale = if original_width > max_width {
max_width / original_width
} else {
1.0 };
let new_width = (original_width * scale) as u32;
let new_height = (original_height * scale) as u32;
let thumbnail = image::imageops::resize(
&original_image,
new_width,
new_height,
image::imageops::FilterType::Lanczos3,
);
Ok(thumbnail)
}
}