use chrono::Utc;
use core_foundation::base::{CFType, TCFType};
use core_foundation::dictionary::CFDictionary;
use core_foundation::number::CFNumber;
use core_foundation::string::CFString;
use core_graphics::display::CGDisplay;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_graphics::image::CGImageRef;
use core_graphics::window::{
copy_window_info, create_image, kCGWindowImageBoundsIgnoreFraming,
kCGWindowListOptionIncludingWindow, kCGWindowListOptionOnScreenOnly, kCGWindowNumber,
};
use crate::errors::{CarDesktopError, Result};
use crate::models::{DisplayId, Frame, WindowHandle};
pub fn capture_display_impl(display: DisplayId) -> Result<Frame> {
let cg_display = if display == DisplayId::PRIMARY {
CGDisplay::main()
} else {
CGDisplay::new(display.0 as u32)
};
let image = cg_display.image().ok_or_else(|| CarDesktopError::OsApi {
detail: format!(
"CGDisplayCreateImage returned null for display {}",
display.0
),
source: None,
})?;
frame_from_cgimage(&image)
}
pub fn capture_window_impl(handle: WindowHandle) -> Result<Frame> {
let frame = lookup_window_frame(handle)?;
let rect = CGRect::new(
&CGPoint::new(frame.x, frame.y),
&CGSize::new(frame.width.max(1.0), frame.height.max(1.0)),
);
let image = create_image(
rect,
kCGWindowListOptionIncludingWindow,
handle.window_id as u32,
kCGWindowImageBoundsIgnoreFraming,
)
.ok_or_else(|| CarDesktopError::OsApi {
detail: format!(
"CGWindowListCreateImage returned null for window {}:{}",
handle.pid, handle.window_id
),
source: None,
})?;
frame_from_cgimage(&image)
}
fn lookup_window_frame(handle: WindowHandle) -> Result<WindowFrame> {
let list = copy_window_info(
kCGWindowListOptionOnScreenOnly | kCGWindowListOptionIncludingWindow,
handle.window_id as u32,
)
.ok_or_else(|| CarDesktopError::OsApi {
detail: "CGWindowListCopyWindowInfo returned null in lookup_window_frame".into(),
source: None,
})?;
let count = list.len();
for i in 0..count {
let Some(dict_ref) = list.get(i) else { continue };
let dict_type_ref = *dict_ref as core_foundation::base::CFTypeRef;
let dict: CFDictionary<CFString, CFType> =
unsafe { CFDictionary::wrap_under_get_rule(dict_type_ref as _) };
let id = {
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowNumber) };
let Some(v) = dict.find(&key) else { continue };
let Some(n): Option<CFNumber> = v.downcast::<CFNumber>() else {
continue;
};
n.to_i64().unwrap_or(-1)
};
if id as u64 != handle.window_id {
continue;
}
return extract_bounds(&dict).ok_or_else(|| CarDesktopError::WindowNotFound {
detail: format!(
"window {}:{} missing kCGWindowBounds",
handle.pid, handle.window_id
),
});
}
Err(CarDesktopError::WindowNotFound {
detail: format!("window {}:{} no longer on-screen", handle.pid, handle.window_id),
})
}
#[derive(Debug, Clone, Copy)]
struct WindowFrame {
x: f64,
y: f64,
width: f64,
height: f64,
}
fn extract_bounds(dict: &CFDictionary<CFString, CFType>) -> Option<WindowFrame> {
let key = unsafe {
CFString::wrap_under_get_rule(core_graphics::window::kCGWindowBounds)
};
let value = dict.find(&key)?;
let untyped: CFDictionary = value.downcast::<CFDictionary>()?;
let bounds: CFDictionary<CFString, CFType> =
unsafe { CFDictionary::wrap_under_get_rule(untyped.as_concrete_TypeRef()) };
let get = |k: &str| -> Option<f64> {
let key = CFString::new(k);
let v = bounds.find(&key)?;
let n: CFNumber = v.downcast::<CFNumber>()?;
n.to_f64().or_else(|| n.to_i64().map(|i| i as f64))
};
Some(WindowFrame {
x: get("X")?,
y: get("Y")?,
width: get("Width")?,
height: get("Height")?,
})
}
fn frame_from_cgimage(image: &CGImageRef) -> Result<Frame> {
let width = image.width() as u32;
let height = image.height() as u32;
let bits_per_pixel = image.bits_per_pixel();
if bits_per_pixel != 32 {
return Err(CarDesktopError::OsApi {
detail: format!(
"unexpected CGImage bits_per_pixel = {bits_per_pixel} (expected 32)"
),
source: None,
});
}
let bytes_per_row = image.bytes_per_row() as usize;
let native = image.data().to_vec();
let stride = width as usize * 4;
let expected_len = bytes_per_row * height as usize;
if native.len() < expected_len {
return Err(CarDesktopError::OsApi {
detail: format!(
"CGImage data length {} less than bytes_per_row*height {}",
native.len(),
expected_len,
),
source: None,
});
}
let mut rgba = Vec::with_capacity(stride * height as usize);
for row in 0..height as usize {
let src_start = row * bytes_per_row;
let src_end = src_start + stride;
for px in native[src_start..src_end].chunks_exact(4) {
let (b, g, r, a) = (px[0], px[1], px[2], px[3]);
rgba.extend_from_slice(&[r, g, b, a]);
}
}
let frame = Frame {
width,
height,
scale_factor: 1.0,
rgba,
captured_at: Utc::now(),
};
frame.validate().map_err(|detail| CarDesktopError::OsApi {
detail,
source: None,
})?;
Ok(frame)
}
#[cfg(test)]
mod tests {
#[test]
fn scale_factor_is_one_for_native_pixel_frames() {
assert!((1.0f32 - 1.0f32).abs() < 1e-6);
}
}