From b5d0f5d4f89c02df151a2b017d8f46f0ada29ed3 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 17 Dec 2025 10:03:01 -0500 Subject: [PATCH] wip --- 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(+) create mode 100644 crates/gpui/examples/capture_zed.rs create mode 100644 crates/gpui/examples/screenshot.rs diff --git a/Cargo.lock b/Cargo.lock index de9cb227c6cfb799099abf446c1bdee61ec85bff..866ec2edd3e348534d0ca249ea4345e5fd8eaa4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7328,6 +7328,7 @@ dependencies = [ "parking_lot", "pathfinder_geometry", "pin-project", + "png 0.17.16", "postage", "pretty_assertions", "profiling", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index da7e660a0171f38b8dd61de1c9323773ded2589b..9c6def6534055d43a89400705ba2e816eec38a6c 100644 --- a/crates/gpui/Cargo.toml +++ b/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" diff --git a/crates/gpui/examples/capture_zed.rs b/crates/gpui/examples/capture_zed.rs new file mode 100644 index 0000000000000000000000000000000000000000..e23b82488f120cb51c48765a8a57db6474cb0085 --- /dev/null +++ b/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 { + 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 { + 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 { + 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> { + 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 = 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!"); + } +} diff --git a/crates/gpui/examples/screenshot.rs b/crates/gpui/examples/screenshot.rs new file mode 100644 index 0000000000000000000000000000000000000000..3d5c0c76df56c0fb885ebd0bb20799feb6b16087 --- /dev/null +++ b/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) -> 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> { + 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> { + 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 = 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 = + 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( + window_handle: &WindowHandle, + cx: &mut App, +) -> Option { + 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( + _window_handle: &WindowHandle, + _cx: &mut App, +) -> Option { + None +}