use objc2_core_foundation::{CFRetained, CGPoint, CGRect, CGSize};
use objc2_core_graphics::{
CGBitmapInfo, CGDataProvider, CGDataProviderCopyData, CGDirectDisplayID, CGDisplayCreateImage,
CGImage, CGImageAlphaInfo, CGImageGetAlphaInfo, CGImageGetBitmapInfo, CGImageGetBytesPerRow,
CGImageGetDataProvider, CGImageGetHeight, CGImageGetWidth, CGMainDisplayID,
CGWindowImageOption, CGWindowListCreateImage, CGWindowListOption,
};
use crate::{Frame, MonitorId, NativeFrame, PixelFormat, PlatformError, Rect, Result};
pub(crate) fn capture_screen(monitor: MonitorId) -> Result<Frame> {
let native = capture_screen_native(monitor)?;
Ok(native_to_packed_rgba(native))
}
pub(crate) fn capture_screen_native(monitor: MonitorId) -> Result<NativeFrame> {
let monitor_bounds = bounds_and_scale_for(monitor).0;
let image: CFRetained<CGImage> = match overlay_window_id_for(monitor) {
Some(wid) => {
let bounds = CGRect {
origin: CGPoint {
x: monitor_bounds.x as f64,
y: monitor_bounds.y as f64,
},
size: CGSize {
width: monitor_bounds.w as f64,
height: monitor_bounds.h as f64,
},
};
CGWindowListCreateImage(
bounds,
CGWindowListOption::OptionOnScreenBelowWindow,
wid,
CGWindowImageOption::Default,
)
.ok_or(PlatformError::Unsupported {
what: "macos: CGWindowListCreateImage returned null \
(Screen Recording permission may not be granted)",
})?
}
None => {
let display_id = cg_display_id_for(monitor);
CGDisplayCreateImage(display_id).ok_or(PlatformError::Unsupported {
what: "macos: CGDisplayCreateImage returned null \
(Screen Recording permission may not be granted)",
})?
}
};
let width = unsafe { CGImageGetWidth(Some(&image)) } as u32;
let height = unsafe { CGImageGetHeight(Some(&image)) } as u32;
let stride = unsafe { CGImageGetBytesPerRow(Some(&image)) } as u32;
let bitmap = unsafe { CGImageGetBitmapInfo(Some(&image)) };
let alpha = unsafe { CGImageGetAlphaInfo(Some(&image)) };
let format = pixel_format_from_cg(bitmap, alpha);
let provider_nn = unsafe { CGImageGetDataProvider(Some(&image)) }.ok_or(
PlatformError::Unsupported { what: "macos: CGImageGetDataProvider returned null" },
)?;
let provider: &CGDataProvider = unsafe { provider_nn.as_ref() };
let data: CFRetained<objc2_core_foundation::CFData> =
CGDataProviderCopyData(Some(provider)).ok_or(PlatformError::Unsupported {
what: "macos: CGDataProviderCopyData returned null",
})?;
let len = data.length() as usize;
let ptr = data.byte_ptr();
if ptr.is_null() {
return Err(PlatformError::Unsupported {
what: "macos: CFDataGetBytePtr returned null",
});
}
let pixels = unsafe { std::slice::from_raw_parts(ptr, len) }.to_vec();
let (bounds, scale_factor) = bounds_and_scale_for(monitor);
Ok(NativeFrame {
width,
height,
stride,
format,
bounds,
scale_factor,
pixels,
})
}
fn cg_display_id_for(monitor: MonitorId) -> CGDirectDisplayID {
use objc2::MainThreadMarker;
use objc2_app_kit::NSScreen;
use objc2_foundation::NSString;
let mtm = match MainThreadMarker::new() {
Some(m) => m,
None => return unsafe { CGMainDisplayID() },
};
let screens = NSScreen::screens(mtm);
let idx = monitor.0 as usize;
let Some(screen) = screens.iter().nth(idx) else {
return unsafe { CGMainDisplayID() };
};
let desc = unsafe { screen.deviceDescription() };
let key = NSString::from_str("NSScreenNumber");
let value = desc.objectForKey(&key);
let Some(value) = value else {
return unsafe { CGMainDisplayID() };
};
let id: u32 = unsafe { objc2::msg_send![&*value, unsignedIntValue] };
if id == 0 {
unsafe { CGMainDisplayID() }
} else {
id
}
}
fn overlay_window_id_for(monitor: MonitorId) -> Option<u32> {
super::app::run_on_main_sync(move || {
super::with_main_state(|s| {
s.overlays.get(&monitor).and_then(|o| {
o.window
.isVisible()
.then(|| o.window.windowNumber() as u32)
})
})
})
}
fn bounds_and_scale_for(monitor: MonitorId) -> (Rect, f64) {
let monitors = super::monitor::monitors().unwrap_or_default();
monitors
.into_iter()
.find(|m| m.id == monitor)
.map(|m| (m.bounds, m.scale_factor))
.unwrap_or((Rect::default(), 1.0))
}
fn pixel_format_from_cg(bitmap: CGBitmapInfo, alpha: CGImageAlphaInfo) -> PixelFormat {
let little_endian = (bitmap.0 & CGBitmapInfo::ByteOrder32Little.0)
== CGBitmapInfo::ByteOrder32Little.0;
let alpha_first = matches!(
alpha,
CGImageAlphaInfo::PremultipliedFirst
| CGImageAlphaInfo::First
| CGImageAlphaInfo::NoneSkipFirst
);
let has_alpha = !matches!(
alpha,
CGImageAlphaInfo::NoneSkipFirst | CGImageAlphaInfo::NoneSkipLast
);
match (little_endian, alpha_first, has_alpha) {
(true, true, true) => PixelFormat::Bgra8,
(true, true, false) => PixelFormat::Bgrx8,
(true, false, true) => PixelFormat::Rgba8,
(true, false, false) => PixelFormat::Rgbx8,
(false, true, true) => PixelFormat::Xrgb8,
(false, true, false) => PixelFormat::Xrgb8,
(false, false, true) => PixelFormat::Xbgr8,
(false, false, false) => PixelFormat::Xbgr8,
}
}
fn native_to_packed_rgba(native: NativeFrame) -> Frame {
let w = native.width as usize;
let h = native.height as usize;
let stride = native.stride as usize;
let row_used = w * 4;
let mut out = Vec::with_capacity(w * h * 4);
let swizzle = matches!(
native.format,
PixelFormat::Bgra8 | PixelFormat::Bgrx8 | PixelFormat::Xbgr8
);
let drop_alpha_skip_first = matches!(native.format, PixelFormat::Xrgb8);
for row in 0..h {
let row_start = row * stride;
let row_end = row_start + row_used;
if row_end > native.pixels.len() {
break;
}
let src = &native.pixels[row_start..row_end];
if swizzle {
for chunk in src.chunks_exact(4) {
out.extend_from_slice(&[chunk[2], chunk[1], chunk[0], chunk[3]]);
}
} else if drop_alpha_skip_first {
for chunk in src.chunks_exact(4) {
out.extend_from_slice(&[chunk[1], chunk[2], chunk[3], chunk[0]]);
}
} else {
out.extend_from_slice(src);
}
}
Frame {
width: native.width,
height: native.height,
scale_factor: native.scale_factor,
bounds: native.bounds,
pixels: out,
}
}