wip

Richard Feldman created

Change summary

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(+)

Detailed changes

Cargo.lock 🔗

@@ -7328,6 +7328,7 @@ dependencies = [
  "parking_lot",
  "pathfinder_geometry",
  "pin-project",
+ "png 0.17.16",
  "postage",
  "pretty_assertions",
  "profiling",

crates/gpui/Cargo.toml 🔗

@@ -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"

crates/gpui/examples/capture_zed.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!");
+    }
+}

crates/gpui/examples/screenshot.rs 🔗

@@ -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
+}