Cargo.lock 🔗
@@ -7328,6 +7328,7 @@ dependencies = [
"parking_lot",
"pathfinder_geometry",
"pin-project",
+ "png 0.17.16",
"postage",
"pretty_assertions",
"profiling",
Richard Feldman created
Cargo.lock | 1
crates/gpui/Cargo.toml | 9
crates/gpui/examples/capture_zed.rs | 447 +++++++++++++++++++++++++++++++
crates/gpui/examples/screenshot.rs | 428 +++++++++++++++++++++++++++++
4 files changed, 885 insertions(+)
@@ -7328,6 +7328,7 @@ dependencies = [
"parking_lot",
"pathfinder_geometry",
"pin-project",
+ "png 0.17.16",
"postage",
"pretty_assertions",
"profiling",
@@ -253,6 +253,7 @@ rand.workspace = true
reqwest_client = { workspace = true, features = ["test-support"] }
unicode-segmentation.workspace = true
util = { workspace = true, features = ["test-support"] }
+png = "0.17"
[target.'cfg(target_os = "windows")'.build-dependencies]
embed-resource = "3.0"
@@ -331,6 +332,14 @@ path = "examples/window_shadow.rs"
name = "grid_layout"
path = "examples/grid_layout.rs"
+[[example]]
+name = "screenshot"
+path = "examples/screenshot.rs"
+
+[[example]]
+name = "capture_zed"
+path = "examples/capture_zed.rs"
+
[[example]]
name = "mouse_pressure"
path = "examples/mouse_pressure.rs"
@@ -0,0 +1,447 @@
+//! Utility: Capture Screenshots of Running Zed Windows
+//!
+//! This utility finds running Zed windows and captures screenshots of them.
+//! It can be used for debugging, documentation, or visual testing.
+//!
+//! Usage:
+//! cargo run -p gpui --example capture_zed
+//!
+//! Options (via environment variables):
+//! CAPTURE_OUTPUT_DIR - Directory to save screenshots (default: current directory)
+//! CAPTURE_WINDOW_INDEX - Which Zed window to capture, 0-indexed (default: all)
+//!
+//! Note: This requires macOS and Screen Recording permissions.
+//! The first time you run this, macOS will prompt you to grant permission.
+
+use std::path::PathBuf;
+
+fn main() {
+ #[cfg(target_os = "macos")]
+ {
+ macos::run();
+ }
+
+ #[cfg(not(target_os = "macos"))]
+ {
+ eprintln!("This utility only works on macOS");
+ std::process::exit(1);
+ }
+}
+
+#[cfg(target_os = "macos")]
+mod macos {
+ use std::path::PathBuf;
+
+ // FFI declarations for CoreGraphics window list
+ #[link(name = "CoreGraphics", kind = "framework")]
+ unsafe extern "C" {
+ fn CGWindowListCopyWindowInfo(option: u32, relativeToWindow: u32) -> CFArrayRef;
+ fn CGWindowListCreateImage(
+ rect: CGRect,
+ list_option: u32,
+ window_id: u32,
+ image_option: u32,
+ ) -> CGImageRef;
+ fn CGImageGetWidth(image: CGImageRef) -> usize;
+ fn CGImageGetHeight(image: CGImageRef) -> usize;
+ fn CGImageGetBytesPerRow(image: CGImageRef) -> usize;
+ fn CGImageGetDataProvider(image: CGImageRef) -> CGDataProviderRef;
+ fn CGImageRelease(image: CGImageRef);
+ fn CGDataProviderCopyData(provider: CGDataProviderRef) -> CFDataRef;
+ }
+
+ #[link(name = "CoreFoundation", kind = "framework")]
+ unsafe extern "C" {
+ fn CFArrayGetCount(array: CFArrayRef) -> isize;
+ fn CFArrayGetValueAtIndex(array: CFArrayRef, idx: isize) -> *const std::ffi::c_void;
+ fn CFDictionaryGetValue(
+ dict: CFDictionaryRef,
+ key: *const std::ffi::c_void,
+ ) -> *const std::ffi::c_void;
+ fn CFStringCreateWithCString(
+ alloc: *const std::ffi::c_void,
+ cstr: *const i8,
+ encoding: u32,
+ ) -> CFStringRef;
+ fn CFStringGetCStringPtr(string: CFStringRef, encoding: u32) -> *const i8;
+ fn CFNumberGetValue(
+ number: CFNumberRef,
+ theType: i32,
+ valuePtr: *mut std::ffi::c_void,
+ ) -> bool;
+ fn CFDataGetLength(data: CFDataRef) -> isize;
+ fn CFDataGetBytePtr(data: CFDataRef) -> *const u8;
+ fn CFRelease(cf: *const std::ffi::c_void);
+ }
+
+ type CFArrayRef = *const std::ffi::c_void;
+ type CFDictionaryRef = *const std::ffi::c_void;
+ type CFStringRef = *const std::ffi::c_void;
+ type CFNumberRef = *const std::ffi::c_void;
+ type CGImageRef = *mut std::ffi::c_void;
+ type CGDataProviderRef = *mut std::ffi::c_void;
+ type CFDataRef = *mut std::ffi::c_void;
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ struct CGPoint {
+ x: f64,
+ y: f64,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ struct CGSize {
+ width: f64,
+ height: f64,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ struct CGRect {
+ origin: CGPoint,
+ size: CGSize,
+ }
+
+ impl CGRect {
+ fn null() -> Self {
+ CGRect {
+ origin: CGPoint {
+ x: f64::INFINITY,
+ y: f64::INFINITY,
+ },
+ size: CGSize {
+ width: 0.0,
+ height: 0.0,
+ },
+ }
+ }
+ }
+
+ // Constants
+ #[allow(non_upper_case_globals)]
+ const kCGWindowListOptionOnScreenOnly: u32 = 1 << 0;
+ #[allow(non_upper_case_globals)]
+ const kCGWindowListExcludeDesktopElements: u32 = 1 << 4;
+ #[allow(non_upper_case_globals)]
+ const kCGWindowListOptionIncludingWindow: u32 = 1 << 3;
+ #[allow(non_upper_case_globals)]
+ const kCGWindowImageBoundsIgnoreFraming: u32 = 1 << 0;
+ #[allow(non_upper_case_globals)]
+ const kCFStringEncodingUTF8: u32 = 0x08000100;
+ #[allow(non_upper_case_globals)]
+ const kCFNumberSInt32Type: i32 = 3;
+
+ #[derive(Debug)]
+ struct WindowInfo {
+ window_id: u32,
+ owner_name: String,
+ window_name: String,
+ bounds: (f64, f64, f64, f64), // x, y, width, height
+ }
+
+ fn get_cf_string(key: &str) -> CFStringRef {
+ unsafe {
+ let cstr = std::ffi::CString::new(key).unwrap();
+ CFStringCreateWithCString(std::ptr::null(), cstr.as_ptr(), kCFStringEncodingUTF8)
+ }
+ }
+
+ fn cf_string_to_rust(cf_string: CFStringRef) -> Option<String> {
+ if cf_string.is_null() {
+ return None;
+ }
+ unsafe {
+ let ptr = CFStringGetCStringPtr(cf_string, kCFStringEncodingUTF8);
+ if ptr.is_null() {
+ return None;
+ }
+ Some(std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned())
+ }
+ }
+
+ fn cf_number_to_i32(cf_number: CFNumberRef) -> Option<i32> {
+ if cf_number.is_null() {
+ return None;
+ }
+ unsafe {
+ let mut value: i32 = 0;
+ if CFNumberGetValue(
+ cf_number,
+ kCFNumberSInt32Type,
+ &mut value as *mut i32 as *mut std::ffi::c_void,
+ ) {
+ Some(value)
+ } else {
+ None
+ }
+ }
+ }
+
+ fn get_zed_windows() -> Vec<WindowInfo> {
+ let mut windows = Vec::new();
+
+ unsafe {
+ let window_list = CGWindowListCopyWindowInfo(
+ kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
+ 0,
+ );
+
+ if window_list.is_null() {
+ return windows;
+ }
+
+ let count = CFArrayGetCount(window_list);
+
+ let key_owner_name = get_cf_string("kCGWindowOwnerName");
+ let key_window_name = get_cf_string("kCGWindowName");
+ let key_window_number = get_cf_string("kCGWindowNumber");
+ let key_bounds = get_cf_string("kCGWindowBounds");
+ let key_x = get_cf_string("X");
+ let key_y = get_cf_string("Y");
+ let key_width = get_cf_string("Width");
+ let key_height = get_cf_string("Height");
+
+ for i in 0..count {
+ let dict = CFArrayGetValueAtIndex(window_list, i) as CFDictionaryRef;
+ if dict.is_null() {
+ continue;
+ }
+
+ // Get owner name
+ let owner_name_cf = CFDictionaryGetValue(dict, key_owner_name) as CFStringRef;
+ let owner_name = cf_string_to_rust(owner_name_cf).unwrap_or_default();
+
+ // Check if this is a Zed window
+ if !owner_name.contains("Zed") {
+ continue;
+ }
+
+ // Get window name
+ let window_name_cf = CFDictionaryGetValue(dict, key_window_name) as CFStringRef;
+ let window_name = cf_string_to_rust(window_name_cf).unwrap_or_default();
+
+ // Get window ID
+ let window_number_cf = CFDictionaryGetValue(dict, key_window_number) as CFNumberRef;
+ let window_id = cf_number_to_i32(window_number_cf).unwrap_or(0) as u32;
+
+ // Get bounds
+ let bounds_dict = CFDictionaryGetValue(dict, key_bounds) as CFDictionaryRef;
+ let (x, y, width, height) = if !bounds_dict.is_null() {
+ let x_cf = CFDictionaryGetValue(bounds_dict, key_x) as CFNumberRef;
+ let y_cf = CFDictionaryGetValue(bounds_dict, key_y) as CFNumberRef;
+ let w_cf = CFDictionaryGetValue(bounds_dict, key_width) as CFNumberRef;
+ let h_cf = CFDictionaryGetValue(bounds_dict, key_height) as CFNumberRef;
+
+ (
+ cf_number_to_i32(x_cf).unwrap_or(0) as f64,
+ cf_number_to_i32(y_cf).unwrap_or(0) as f64,
+ cf_number_to_i32(w_cf).unwrap_or(0) as f64,
+ cf_number_to_i32(h_cf).unwrap_or(0) as f64,
+ )
+ } else {
+ (0.0, 0.0, 0.0, 0.0)
+ };
+
+ // Skip windows with zero size (like menu bar items)
+ if width < 100.0 || height < 100.0 {
+ continue;
+ }
+
+ windows.push(WindowInfo {
+ window_id,
+ owner_name,
+ window_name,
+ bounds: (x, y, width, height),
+ });
+ }
+
+ // Clean up CF strings
+ CFRelease(key_owner_name);
+ CFRelease(key_window_name);
+ CFRelease(key_window_number);
+ CFRelease(key_bounds);
+ CFRelease(key_x);
+ CFRelease(key_y);
+ CFRelease(key_width);
+ CFRelease(key_height);
+ CFRelease(window_list);
+ }
+
+ windows
+ }
+
+ fn capture_window_to_png(
+ window_id: u32,
+ output_path: &std::path::Path,
+ ) -> Result<(usize, usize), Box<dyn std::error::Error>> {
+ use std::fs::File;
+ use std::io::BufWriter;
+
+ // Capture the window
+ let image = unsafe {
+ CGWindowListCreateImage(
+ CGRect::null(),
+ kCGWindowListOptionIncludingWindow,
+ window_id,
+ kCGWindowImageBoundsIgnoreFraming,
+ )
+ };
+
+ if image.is_null() {
+ return Err("Failed to capture window - image is null. \
+ Make sure Screen Recording permission is granted in \
+ System Preferences > Privacy & Security > Screen Recording."
+ .into());
+ }
+
+ // Get image dimensions
+ let width = unsafe { CGImageGetWidth(image) };
+ let height = unsafe { CGImageGetHeight(image) };
+
+ if width == 0 || height == 0 {
+ unsafe { CGImageRelease(image) };
+ return Err("Captured image has zero dimensions".into());
+ }
+
+ // Get the image data
+ let data_provider = unsafe { CGImageGetDataProvider(image) };
+ if data_provider.is_null() {
+ unsafe { CGImageRelease(image) };
+ return Err("Failed to get image data provider".into());
+ }
+
+ let data = unsafe { CGDataProviderCopyData(data_provider) };
+ if data.is_null() {
+ unsafe { CGImageRelease(image) };
+ return Err("Failed to copy image data".into());
+ }
+
+ let length = unsafe { CFDataGetLength(data) } as usize;
+ let ptr = unsafe { CFDataGetBytePtr(data) };
+ let bytes = unsafe { std::slice::from_raw_parts(ptr, length) };
+ let bytes_per_row = unsafe { CGImageGetBytesPerRow(image) };
+
+ // The image is in BGRA format with potential row padding, convert to RGBA for PNG
+ let mut rgba_bytes = Vec::with_capacity(width * height * 4);
+ for row in 0..height {
+ let row_start = row * bytes_per_row;
+ for col in 0..width {
+ let pixel_start = row_start + col * 4;
+ if pixel_start + 3 < length {
+ rgba_bytes.push(bytes[pixel_start + 2]); // R (was B)
+ rgba_bytes.push(bytes[pixel_start + 1]); // G
+ rgba_bytes.push(bytes[pixel_start]); // B (was R)
+ rgba_bytes.push(bytes[pixel_start + 3]); // A
+ }
+ }
+ }
+
+ // Write PNG file
+ let file = File::create(output_path)?;
+ let w = BufWriter::new(file);
+ let mut encoder = png::Encoder::new(w, width as u32, height as u32);
+ encoder.set_color(png::ColorType::Rgba);
+ encoder.set_depth(png::BitDepth::Eight);
+ let mut writer = encoder.write_header()?;
+ writer.write_image_data(&rgba_bytes)?;
+
+ // Cleanup
+ unsafe {
+ CFRelease(data as *const _);
+ CGImageRelease(image);
+ }
+
+ Ok((width, height))
+ }
+
+ pub fn run() {
+ println!("Looking for Zed windows...\n");
+
+ let windows = get_zed_windows();
+
+ if windows.is_empty() {
+ eprintln!("No Zed windows found!");
+ eprintln!("\nMake sure Zed is running and visible on screen.");
+ eprintln!("Note: Minimized windows cannot be captured.");
+ std::process::exit(1);
+ }
+
+ println!("Found {} Zed window(s):\n", windows.len());
+ for (i, window) in windows.iter().enumerate() {
+ println!(
+ " [{}] Window ID: {}, Title: \"{}\", Size: {}x{}",
+ i, window.window_id, window.window_name, window.bounds.2, window.bounds.3
+ );
+ }
+ println!();
+
+ // Get output directory
+ let output_dir = std::env::var("CAPTURE_OUTPUT_DIR")
+ .map(PathBuf::from)
+ .unwrap_or_else(|_| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
+
+ // Get window index filter
+ let window_index_filter: Option<usize> = std::env::var("CAPTURE_WINDOW_INDEX")
+ .ok()
+ .and_then(|s| s.parse().ok());
+
+ // Capture windows
+ let windows_to_capture: Vec<_> = match window_index_filter {
+ Some(idx) => {
+ if idx < windows.len() {
+ vec![&windows[idx]]
+ } else {
+ eprintln!(
+ "Window index {} is out of range (0-{})",
+ idx,
+ windows.len() - 1
+ );
+ std::process::exit(1);
+ }
+ }
+ None => windows.iter().collect(),
+ };
+
+ println!("Capturing {} window(s)...\n", windows_to_capture.len());
+
+ for (i, window) in windows_to_capture.iter().enumerate() {
+ let filename = if window.window_name.is_empty() {
+ format!("zed_window_{}.png", i)
+ } else {
+ // Sanitize window name for filename
+ let safe_name: String = window
+ .window_name
+ .chars()
+ .map(|c| {
+ if c.is_alphanumeric() || c == '-' || c == '_' {
+ c
+ } else {
+ '_'
+ }
+ })
+ .collect();
+ format!("zed_{}.png", safe_name)
+ };
+
+ let output_path = output_dir.join(&filename);
+
+ match capture_window_to_png(window.window_id, &output_path) {
+ Ok((width, height)) => {
+ println!(
+ "✓ Captured \"{}\" -> {} ({}x{})",
+ window.window_name,
+ output_path.display(),
+ width,
+ height
+ );
+ }
+ Err(e) => {
+ eprintln!("✗ Failed to capture \"{}\": {}", window.window_name, e);
+ }
+ }
+ }
+
+ println!("\nDone!");
+ }
+}
@@ -0,0 +1,428 @@
+//! Example: Off-screen Window Rendering with Screenshots
+//!
+//! This example demonstrates how to:
+//! 1. Create a window positioned off-screen (so it's not visible to the user)
+//! 2. Render real GPUI content using Metal
+//! 3. Take screenshots of the window using CGWindowListCreateImage
+//! 4. Save the screenshots as PNG files
+//!
+//! This is useful for automated visual testing where you want real rendering
+//! but don't want windows appearing on screen.
+//!
+//! Usage:
+//! cargo run -p gpui --example screenshot
+//!
+//! Note: This requires macOS and Screen Recording permissions.
+//! The first time you run this, macOS will prompt you to grant permission.
+
+use gpui::{
+ App, AppContext, Application, Bounds, Context, Entity, IntoElement, Render, SharedString,
+ Window, WindowBounds, WindowHandle, WindowOptions, div, point, prelude::*, px, rgb, size,
+};
+use raw_window_handle::{HasWindowHandle, RawWindowHandle};
+use std::path::PathBuf;
+use std::time::Duration;
+
+// ============================================================================
+// GPUI View to Render
+// ============================================================================
+
+struct ScreenshotDemo {
+ counter: u32,
+ message: SharedString,
+}
+
+impl ScreenshotDemo {
+ fn new() -> Self {
+ Self {
+ counter: 0,
+ message: "Hello, Screenshot!".into(),
+ }
+ }
+
+ fn increment(&mut self) {
+ self.counter += 1;
+ self.message = format!("Counter: {}", self.counter).into();
+ }
+}
+
+impl Render for ScreenshotDemo {
+ fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .flex()
+ .flex_col()
+ .gap_4()
+ .bg(rgb(0x1e1e2e)) // Dark background
+ .size_full()
+ .justify_center()
+ .items_center()
+ .child(
+ div()
+ .text_3xl()
+ .text_color(rgb(0xcdd6f4))
+ .child(self.message.clone()),
+ )
+ .child(
+ div()
+ .flex()
+ .gap_3()
+ .child(colored_box(rgb(0xf38ba8))) // Red
+ .child(colored_box(rgb(0xa6e3a1))) // Green
+ .child(colored_box(rgb(0x89b4fa))) // Blue
+ .child(colored_box(rgb(0xf9e2af))), // Yellow
+ )
+ .child(
+ div()
+ .mt_4()
+ .px_4()
+ .py_2()
+ .bg(rgb(0x313244))
+ .rounded_md()
+ .text_color(rgb(0xbac2de))
+ .child(format!("Frame: {}", self.counter)),
+ )
+ }
+}
+
+fn colored_box(color: gpui::Rgba) -> impl IntoElement {
+ div()
+ .size_16()
+ .bg(color)
+ .rounded_lg()
+ .shadow_md()
+ .border_2()
+ .border_color(rgb(0x45475a))
+}
+
+// ============================================================================
+// Screenshot Capture (macOS-specific using CGWindowListCreateImage)
+// ============================================================================
+
+#[cfg(target_os = "macos")]
+mod screenshot {
+ use std::path::Path;
+
+ // FFI declarations for CoreGraphics
+ #[link(name = "CoreGraphics", kind = "framework")]
+ unsafe extern "C" {
+ fn CGWindowListCreateImage(
+ rect: CGRect,
+ list_option: u32,
+ window_id: u32,
+ image_option: u32,
+ ) -> CGImageRef;
+
+ fn CGImageGetWidth(image: CGImageRef) -> usize;
+ fn CGImageGetHeight(image: CGImageRef) -> usize;
+ fn CGImageGetDataProvider(image: CGImageRef) -> CGDataProviderRef;
+ fn CGImageRelease(image: CGImageRef);
+ fn CGDataProviderCopyData(provider: CGDataProviderRef) -> CFDataRef;
+ }
+
+ #[link(name = "CoreFoundation", kind = "framework")]
+ unsafe extern "C" {
+ fn CFDataGetLength(data: CFDataRef) -> isize;
+ fn CFDataGetBytePtr(data: CFDataRef) -> *const u8;
+ fn CFRelease(cf: *const std::ffi::c_void);
+ }
+
+ type CGImageRef = *mut std::ffi::c_void;
+ type CGDataProviderRef = *mut std::ffi::c_void;
+ type CFDataRef = *mut std::ffi::c_void;
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ struct CGPoint {
+ x: f64,
+ y: f64,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ struct CGSize {
+ width: f64,
+ height: f64,
+ }
+
+ #[repr(C)]
+ #[derive(Copy, Clone)]
+ struct CGRect {
+ origin: CGPoint,
+ size: CGSize,
+ }
+
+ impl CGRect {
+ fn null() -> Self {
+ CGRect {
+ origin: CGPoint {
+ x: f64::INFINITY,
+ y: f64::INFINITY,
+ },
+ size: CGSize {
+ width: 0.0,
+ height: 0.0,
+ },
+ }
+ }
+ }
+
+ #[allow(non_upper_case_globals)]
+ const kCGWindowListOptionIncludingWindow: u32 = 1 << 3;
+ #[allow(non_upper_case_globals)]
+ const kCGWindowImageBoundsIgnoreFraming: u32 = 1 << 0;
+
+ /// Captures a screenshot of the specified window and saves it as a PNG.
+ pub fn capture_window_to_png(
+ window_number: i64,
+ output_path: &Path,
+ ) -> Result<(), Box<dyn std::error::Error>> {
+ use std::fs::File;
+ use std::io::BufWriter;
+
+ // Capture the window
+ let image = unsafe {
+ CGWindowListCreateImage(
+ CGRect::null(),
+ kCGWindowListOptionIncludingWindow,
+ window_number as u32,
+ kCGWindowImageBoundsIgnoreFraming,
+ )
+ };
+
+ if image.is_null() {
+ return Err("Failed to capture window - image is null. \
+ Make sure Screen Recording permission is granted in \
+ System Preferences > Privacy & Security > Screen Recording."
+ .into());
+ }
+
+ // Get image dimensions
+ let width = unsafe { CGImageGetWidth(image) };
+ let height = unsafe { CGImageGetHeight(image) };
+
+ if width == 0 || height == 0 {
+ unsafe { CGImageRelease(image) };
+ return Err("Captured image has zero dimensions".into());
+ }
+
+ // Get the image data
+ let data_provider = unsafe { CGImageGetDataProvider(image) };
+ if data_provider.is_null() {
+ unsafe { CGImageRelease(image) };
+ return Err("Failed to get image data provider".into());
+ }
+
+ let data = unsafe { CGDataProviderCopyData(data_provider) };
+ if data.is_null() {
+ unsafe { CGImageRelease(image) };
+ return Err("Failed to copy image data".into());
+ }
+
+ let length = unsafe { CFDataGetLength(data) } as usize;
+ let ptr = unsafe { CFDataGetBytePtr(data) };
+ let bytes = unsafe { std::slice::from_raw_parts(ptr, length) };
+
+ // The image is in BGRA format, convert to RGBA for PNG
+ let mut rgba_bytes = Vec::with_capacity(length);
+ for chunk in bytes.chunks(4) {
+ if chunk.len() == 4 {
+ rgba_bytes.push(chunk[2]); // R (was B)
+ rgba_bytes.push(chunk[1]); // G
+ rgba_bytes.push(chunk[0]); // B (was R)
+ rgba_bytes.push(chunk[3]); // A
+ }
+ }
+
+ // Write PNG file
+ let file = File::create(output_path)?;
+ let w = BufWriter::new(file);
+ let mut encoder = png::Encoder::new(w, width as u32, height as u32);
+ encoder.set_color(png::ColorType::Rgba);
+ encoder.set_depth(png::BitDepth::Eight);
+ let mut writer = encoder.write_header()?;
+ writer.write_image_data(&rgba_bytes)?;
+
+ // Cleanup
+ unsafe {
+ CFRelease(data as *const _);
+ CGImageRelease(image);
+ }
+
+ println!(
+ "Screenshot saved to {} ({}x{})",
+ output_path.display(),
+ width,
+ height
+ );
+ Ok(())
+ }
+}
+
+#[cfg(not(target_os = "macos"))]
+mod screenshot {
+ use std::path::Path;
+
+ pub fn capture_window_to_png(
+ _window_number: i64,
+ _output_path: &Path,
+ ) -> Result<(), Box<dyn std::error::Error>> {
+ Err("Screenshot capture is only supported on macOS".into())
+ }
+}
+
+// ============================================================================
+// Main Application
+// ============================================================================
+
+fn main() {
+ env_logger::init();
+
+ Application::new().run(|cx: &mut App| {
+ // Position the window FAR off-screen so it's not visible
+ // but macOS still renders it (unlike minimized/hidden windows)
+ let off_screen_origin = point(px(-10000.0), px(-10000.0));
+ let window_size = size(px(800.0), px(600.0));
+
+ let bounds = Bounds {
+ origin: off_screen_origin,
+ size: window_size,
+ };
+
+ println!("Creating off-screen window at {:?}", bounds);
+ println!("(The window is positioned off-screen but is still being rendered by macOS)");
+
+ // Open the window
+ let window_handle: WindowHandle<ScreenshotDemo> = cx
+ .open_window(
+ WindowOptions {
+ window_bounds: Some(WindowBounds::Windowed(bounds)),
+ focus: false, // Don't steal focus
+ show: true, // Must be true for rendering to occur
+ ..Default::default()
+ },
+ |_, cx| cx.new(|_| ScreenshotDemo::new()),
+ )
+ .expect("Failed to open window");
+
+ // Get the entity for later updates
+ let view_entity: Entity<ScreenshotDemo> =
+ window_handle.entity(cx).expect("Failed to get root entity");
+
+ // Get output directory
+ let output_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
+
+ // Schedule screenshot captures after allowing time for rendering
+ cx.spawn(async move |cx| {
+ // Wait for the window to fully render
+ smol::Timer::after(Duration::from_millis(500)).await;
+
+ // Get the window number for screenshots
+ let window_number = cx
+ .update(|app: &mut App| get_window_number_from_handle(&window_handle, app))
+ .ok()
+ .flatten();
+
+ let Some(window_number) = window_number else {
+ eprintln!("Could not get window number. Are you running on macOS?");
+ let _ = cx.update(|app: &mut App| app.quit());
+ return;
+ };
+
+ println!("Window number: {}", window_number);
+
+ // Take screenshot 1
+ let output_path = output_dir.join("screenshot_1.png");
+ match screenshot::capture_window_to_png(window_number, &output_path) {
+ Ok(()) => println!("✓ Captured screenshot_1.png"),
+ Err(e) => eprintln!("✗ Failed to capture screenshot_1.png: {}", e),
+ }
+
+ // Update the view (update the entity directly, not through window_handle.update)
+ let _ = cx.update_entity(&view_entity, |view: &mut ScreenshotDemo, ecx| {
+ view.increment();
+ view.increment();
+ view.increment();
+ ecx.notify(); // Trigger a re-render
+ });
+
+ // Wait for re-render
+ smol::Timer::after(Duration::from_millis(200)).await;
+
+ // Take screenshot 2
+ let output_path = output_dir.join("screenshot_2.png");
+ match screenshot::capture_window_to_png(window_number, &output_path) {
+ Ok(()) => println!("✓ Captured screenshot_2.png"),
+ Err(e) => eprintln!("✗ Failed to capture screenshot_2.png: {}", e),
+ }
+
+ // Update again
+ let _ = cx.update_entity(&view_entity, |view: &mut ScreenshotDemo, ecx| {
+ for _ in 0..7 {
+ view.increment();
+ }
+ ecx.notify(); // Trigger a re-render
+ });
+
+ // Wait for re-render
+ smol::Timer::after(Duration::from_millis(200)).await;
+
+ // Take screenshot 3
+ let output_path = output_dir.join("screenshot_3.png");
+ match screenshot::capture_window_to_png(window_number, &output_path) {
+ Ok(()) => println!("✓ Captured screenshot_3.png"),
+ Err(e) => eprintln!("✗ Failed to capture screenshot_3.png: {}", e),
+ }
+
+ println!("\nAll screenshots captured!");
+ println!(
+ "Check {} for screenshot_1.png, screenshot_2.png, screenshot_3.png",
+ output_dir.display()
+ );
+
+ // Quit after screenshots are taken
+ smol::Timer::after(Duration::from_millis(500)).await;
+ let _ = cx.update(|app: &mut App| app.quit());
+ })
+ .detach();
+ });
+}
+
+/// Extract the window number from a GPUI WindowHandle using raw_window_handle
+#[cfg(target_os = "macos")]
+fn get_window_number_from_handle<V: 'static + Render>(
+ window_handle: &WindowHandle<V>,
+ cx: &mut App,
+) -> Option<i64> {
+ use objc::{msg_send, sel, sel_impl};
+
+ window_handle
+ .update(cx, |_root: &mut V, window: &mut Window, _cx| {
+ let handle = window.window_handle().ok()?;
+ match handle.as_raw() {
+ RawWindowHandle::AppKit(appkit_handle) => {
+ let ns_view = appkit_handle.ns_view.as_ptr();
+ unsafe {
+ let ns_window: *mut std::ffi::c_void =
+ msg_send![ns_view as cocoa::base::id, window];
+ if ns_window.is_null() {
+ return None;
+ }
+ let window_number: i64 =
+ msg_send![ns_window as cocoa::base::id, windowNumber];
+ Some(window_number)
+ }
+ }
+ _ => None,
+ }
+ })
+ .ok()
+ .flatten()
+}
+
+#[cfg(not(target_os = "macos"))]
+fn get_window_number_from_handle<V: 'static + Render>(
+ _window_handle: &WindowHandle<V>,
+ _cx: &mut App,
+) -> Option<i64> {
+ None
+}