Files
Ely-GPUI-Components/examples/gallery/capture.rs
T
ZacharyZhang-NY 3f1597fa28 Port · Parity: every page matches gpui 0.2.2's; the rules re-tested
Every page and scripted state was shot with gpui 0.2.2 and with the
port, light and dark, full width and at 280px, judged pair by pair by
agents with skeptics, and every page's log diffed. What differed is
fixed, each with a test that fails without it where the test platform
can show it:

- The gallery's harness takes posted mouse moves (gpui_macos listens
  to a tracking area), posts each drag move twice (it no longer
  replays a held drag), and waits three frames before each shot.
- Rolling digits: a block column, so taffy 0.13 lends the cell no
  baseline, and a line of whole device pixels, since gpui snaps each
  length on its own. DescriptionList's row aligns ends.
- Margins that taffy 0.13 collapses through a plain block: the
  drawer's pull and snap, the stagger and transition rise, the dialog
  and palette rises, the fullscreen dialog's slide.
- An Input keeps room to type in a squeezed row; the field rows drop
  w_full, whose reason was taffy 0.9's.

AGENTS.md's rules that named gpui 0.2.2 or taffy 0.9 were re-tested
against both sources and rewritten, and new ones say what the port
changed. TASKS.md's tags follow: what the new gpui makes possible is
prove. Captions, doc comments and README say what holds now.
2026-09-29 10:39:55 -04:00

220 lines
7.4 KiB
Rust

use std::{
path::{Path, PathBuf},
time::Duration,
};
use anyhow::{Context as _, Result, bail};
use ely_gpui_component::theme::Theme;
use gpui::{App, AsyncApp, Window, WindowHandle, px};
use crate::{
pages, script,
shell::{Choice, Gallery},
};
const SETTLE: Duration = Duration::from_millis(700);
const SCROLL_SETTLE: Duration = Duration::from_millis(250);
/// Shoots one page, or all, in both modes, then quits; exits 1 on failure.
pub fn run(
window: WindowHandle<Gallery>,
dir: PathBuf,
only: Option<usize>,
scripted: bool,
cx: &mut App,
) {
Theme::update(cx, |theme| theme.reduced_motion = true);
cx.spawn(async move |cx| {
let outcome = shoot_all(window, &dir, only, scripted, cx).await;
if let Err(error) = outcome.map(|()| cx.update(|cx| cx.quit())) {
log::error!("capture failed: {error:#}");
std::process::exit(1);
}
})
.detach();
}
async fn shoot_all(
window: WindowHandle<Gallery>,
dir: &Path,
only: Option<usize>,
scripted: bool,
cx: &mut AsyncApp,
) -> Result<()> {
std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
let chosen = pages::ALL
.iter()
.enumerate()
.filter(|(ix, _)| only.is_none_or(|only| only == *ix));
let gallery = window.update(cx, |_, window, _| number(window))??;
window.update(cx, |_, window, _| take_posted_moves(window))??;
for (ix, page) in chosen {
for (choice, name) in [(Choice::Light, "light"), (Choice::Dark, "dark")] {
window.update(cx, |gallery, window, cx| {
gallery.select(ix, cx);
gallery.fresh(cx);
gallery.choose(choice, window, cx);
})?;
script::park(window, cx)?;
cx.background_executor().timer(SETTLE).await;
let (viewport, max) = window.update(cx, |gallery, _, _| gallery.scroll_extent())?;
if viewport <= px(0.0) {
bail!("page {} has no viewport", page.slug);
}
let mut offset = px(0.0);
for segment in 0.. {
let suffix = if segment == 0 {
String::new()
} else {
format!("-{segment}")
};
let path = dir.join(format!("{}-{name}{suffix}.png", page.slug));
script::shown(window.into(), cx).await?;
snapshot(gallery, &path)?;
log::info!("capture: wrote {}", path.display());
if offset >= max {
break;
}
offset = (offset + viewport).min(max);
window.update(cx, |gallery, _, cx| gallery.scroll_to(offset, cx))?;
cx.background_executor().timer(SCROLL_SETTLE).await;
}
if scripted && !page.script.is_empty() {
window.update(cx, |gallery, _, cx| gallery.select(ix, cx))?;
cx.background_executor().timer(SETTLE).await;
let shot = |step: &str| dir.join(format!("{}-{step}-{name}.png", page.slug));
script::play(window, page.script, shot, cx).await?;
}
}
}
Ok(())
}
/// A gpui window's AppKit window.
#[cfg(target_os = "macos")]
fn native(window: &Window) -> Result<cocoa::base::id> {
use cocoa::base::id;
use objc::{msg_send, sel, sel_impl};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
let handle = HasWindowHandle::window_handle(window)
.map_err(|error| anyhow::anyhow!("window has no native handle: {error:?}"))?;
let RawWindowHandle::AppKit(appkit) = handle.as_raw() else {
bail!("window is not an AppKit window");
};
Ok(unsafe { msg_send![appkit.ns_view.as_ptr() as id, window] })
}
/// A gpui window's number, as AppKit and CoreGraphics know it.
#[cfg(target_os = "macos")]
pub fn number(window: &Window) -> Result<u32> {
use objc::{msg_send, sel, sel_impl};
let number: i64 = unsafe { msg_send![native(window)?, windowNumber] };
u32::try_from(number).context("window number out of range")
}
/// Takes posted mouse moves; gpui's tracking area takes only real ones.
#[cfg(target_os = "macos")]
fn take_posted_moves(window: &Window) -> Result<()> {
use cocoa::{appkit::NSWindow, base::YES};
unsafe { native(window)?.setAcceptsMouseMovedEvents_(YES) };
log::info!("capture: the window takes posted mouse moves");
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn take_posted_moves(_: &Window) -> Result<()> {
bail!("capture needs macOS window APIs")
}
/// This process's frontmost window above the normal level: a popover or menu.
#[cfg(target_os = "macos")]
pub fn popup_number() -> Result<u32> {
use core_foundation::{
base::{CFType, TCFType},
dictionary::CFDictionary,
number::CFNumber,
string::CFString,
};
use core_graphics::window;
let read = |dict: &CFDictionary<CFString, CFType>, key: &'static str| {
dict.find(CFString::from_static_string(key))
.and_then(|value| value.downcast::<CFNumber>())
.and_then(|value| value.to_i64())
};
let pid = i64::from(std::process::id());
let list = window::copy_window_info(
window::kCGWindowListOptionOnScreenOnly,
window::kCGNullWindowID,
)
.context("window list unavailable")?;
let id = list
.iter()
.map(|item| unsafe { CFDictionary::<CFString, CFType>::wrap_under_get_rule(*item as _) })
.find(|dict| {
read(dict, "kCGWindowOwnerPID") == Some(pid)
&& read(dict, "kCGWindowLayer").is_some_and(|layer| layer > 0)
})
.and_then(|dict| read(&dict, "kCGWindowNumber"))
.context("no popup window of this process is on screen")?;
u32::try_from(id).context("window number out of range")
}
#[cfg(not(target_os = "macos"))]
pub fn popup_number() -> Result<u32> {
bail!("capture needs macOS window APIs")
}
#[cfg(not(target_os = "macos"))]
pub fn number(_: &Window) -> Result<u32> {
bail!("capture needs macOS window APIs")
}
#[cfg(target_os = "macos")]
pub fn snapshot(number: u32, path: &Path) -> Result<()> {
use core_graphics::{
geometry::{CGPoint, CGRect, CGSize},
window,
};
let image = window::create_image(
CGRect::new(&CGPoint::new(0.0, 0.0), &CGSize::new(0.0, 0.0)),
window::kCGWindowListOptionIncludingWindow,
number,
window::kCGWindowImageBoundsIgnoreFraming | window::kCGWindowImageBestResolution,
)
.context("window capture returned nothing")?;
if image.bits_per_pixel() != 32 {
bail!(
"unexpected capture format: {} bits per pixel",
image.bits_per_pixel()
);
}
let (width, height, stride) = (image.width(), image.height(), image.bytes_per_row());
let data = image.data();
let bytes = data.bytes();
let mut rgba = Vec::with_capacity(width * height * 4);
for y in 0..height {
for x in 0..width {
let i = y * stride + x * 4;
rgba.extend_from_slice(&[bytes[i + 2], bytes[i + 1], bytes[i], bytes[i + 3]]);
}
}
image::save_buffer(
path,
&rgba,
width as u32,
height as u32,
image::ColorType::Rgba8,
)
.with_context(|| format!("write {}", path.display()))
}
#[cfg(not(target_os = "macos"))]
pub fn snapshot(_: u32, _: &Path) -> Result<()> {
bail!("capture needs macOS window APIs")
}