Capture · Roots: roots drop w_full; hosts give the width

145 component roots drop w_full, the early returns included. Three
field rows keep it, Input, InputGroup and TagInput: stretched without
a width, taffy 0.9 laid the field's flex_1 share at no width in the
template picker at 280px, and the placeholder drew nothing. Container
keeps it, since mx_auto needs a width to fill.

Hosts that relied on a root's width now give it: MessageList boxes
each row at the list's width, the splash gives its bar the column's,
and test stages that mount a component as the window's root sit in a
size_full box. Tests hold a narrow row across the list and the splash
bar across its column; each goes red without its fix.

The gallery gains --narrow <px>, which lays each page in a padded
card at that width and shoots it without its script. The page
header wraps: the title takes the room and the theme switcher drops
below, where a long summary used to push it past the edge. A file
panel step waits for the panel to become key instead of a fixed
pause, which lost a race in a capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-28 13:07:22 -04:00
1 parent 486f618ecc
commit 5779daef82
126 files changed
+275 -276

No files matched your search

+11 -4
View File
@@ -15,11 +15,17 @@ use crate::{
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>, cx: &mut App) {
/// 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, cx).await;
let outcome = shoot_all(window, &dir, only, scripted, cx).await;
if let Err(error) = outcome.and_then(|()| cx.update(|cx| cx.quit())) {
log::error!("capture failed: {error:#}");
std::process::exit(1);
@@ -32,6 +38,7 @@ 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()))?;
@@ -70,7 +77,7 @@ async fn shoot_all(
window.update(cx, |gallery, _, cx| gallery.scroll_to(offset, cx))?;
cx.background_executor().timer(SCROLL_SETTLE).await;
}
if !page.script.is_empty() {
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));
+13 -2
View File
@@ -68,12 +68,14 @@ fn choose(window: WindowHandle<shell::Gallery>, choice: shell::Choice, cx: &mut
struct Args {
page: Option<usize>,
capture: Option<PathBuf>,
narrow: Option<f32>,
}
fn parse_args() -> Result<Args> {
let mut args = Args {
page: None,
capture: None,
narrow: None,
};
let mut iter = std::env::args().skip(1);
while let Some(flag) = iter.next() {
@@ -85,6 +87,13 @@ fn parse_args() -> Result<Args> {
args.page = Some(pages::find(&value).with_context(|| format!("no page {value}"))?)
}
"--capture" => args.capture = Some(PathBuf::from(value)),
"--narrow" => {
args.narrow = Some(
value
.parse()
.with_context(|| format!("bad width {value}"))?,
)
}
other => bail!("unknown flag {other}"),
}
}
@@ -118,13 +127,15 @@ fn main() -> Result<()> {
};
let window = cx
.open_window(options, |window, cx| {
cx.new(|cx| shell::Gallery::new(args.page.unwrap_or(0), window, cx))
cx.new(|cx| {
shell::Gallery::new(args.page.unwrap_or(0), args.narrow, window, cx)
})
})
.expect("gallery window failed to open");
menu_actions(window, cx);
cx.activate(true);
if let Some(dir) = args.capture {
capture::run(window, dir, args.page, cx);
capture::run(window, dir, args.page, args.narrow.is_none(), cx);
}
});
Ok(())
-2
View File
@@ -114,7 +114,6 @@ const SCRIPT: &[Step] = &[
Step::Shot("hotkey"),
Step::DownAt("path", 380.0, 16.0),
Step::UpAt("path", 380.0, 16.0),
Step::Wait(1200),
Step::CancelPanel,
Step::Wait(600),
Step::DownAt("checks", 40.0, 66.0),
@@ -242,7 +241,6 @@ const SCRIPT: &[Step] = &[
Step::Shot("gradient"),
Step::DownAt("file-one", 100.0, 14.0),
Step::UpAt("file-one", 100.0, 14.0),
Step::Wait(1200),
Step::CancelPanel,
Step::Wait(600),
Step::Shot("files"),
+21 -6
View File
@@ -53,6 +53,8 @@ pub enum Step {
}
const FRAME: Duration = Duration::from_millis(120);
/// How long an out-of-process file panel may take to open.
const PANEL: Duration = Duration::from_secs(10);
const DRAG_STEPS: u32 = 10;
#[derive(Clone, Copy)]
@@ -218,7 +220,15 @@ pub async fn play(
let number = window.update(cx, |_, window, _| number(window))??;
post_key(number, true, text, code)?;
}
Step::CancelPanel => cancel_panel()?,
Step::CancelPanel => {
let mut waited = Duration::ZERO;
while !cancel_panel()? {
anyhow::ensure!(waited < PANEL, "no file panel became key in {PANEL:?}");
cx.background_executor().timer(FRAME).await;
waited += FRAME;
}
log::info!("script: file panel cancelled after {waited:?}");
}
Step::ExpectClosed(key) => {
let handle = opened(key, cx)?;
let open = cx.update(|cx| cx.windows().contains(&handle))?;
@@ -363,7 +373,8 @@ fn post_key(_: u32, _: bool, _: &str, _: u16) -> Result<()> {
}
#[cfg(target_os = "macos")]
fn cancel_panel() -> Result<()> {
/// Cancels a key file panel; false while none is key.
fn cancel_panel() -> Result<bool> {
use cocoa::{
appkit::NSApp,
base::{BOOL, NO, id, nil},
@@ -371,16 +382,20 @@ fn cancel_panel() -> Result<()> {
use objc::{class, msg_send, sel, sel_impl};
unsafe {
let key: id = msg_send![NSApp(), keyWindow];
anyhow::ensure!(key != nil, "no key window to cancel");
if key == nil {
return Ok(false);
}
let panel: BOOL = msg_send![key, isKindOfClass: class!(NSSavePanel)];
anyhow::ensure!(panel != NO, "the key window is not a file panel");
if panel == NO {
return Ok(false);
}
let _: () = msg_send![key, cancel: nil];
}
Ok(())
Ok(true)
}
#[cfg(not(target_os = "macos"))]
fn cancel_panel() -> Result<()> {
fn cancel_panel() -> Result<bool> {
anyhow::bail!("file panels need macOS")
}
+63 -30
View File
@@ -5,9 +5,9 @@ use ely_gpui_component::{
theme::{ActiveTheme, ControlSize, Mode, Radius, TextSize, Theme},
};
use gpui::{
Bounds, Context, FocusHandle, FontWeight, InteractiveElement, IntoElement, ParentElement,
Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Subscription,
Window, div, point, prelude::*, px,
AnyElement, App, Bounds, Context, FocusHandle, FontWeight, InteractiveElement, IntoElement,
ParentElement, Pixels, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
Subscription, Window, div, point, prelude::*, px,
};
use crate::pages;
@@ -26,11 +26,18 @@ pub struct Gallery {
choice: Choice,
focus: FocusHandle,
scroll: ScrollHandle,
/// Narrow check width, if any.
narrow: Option<Pixels>,
_appearance: Subscription,
}
impl Gallery {
pub fn new(page: usize, window: &mut Window, cx: &mut Context<Self>) -> Self {
pub fn new(
page: usize,
narrow: Option<f32>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let appearance = cx.observe_window_appearance(window, |gallery, window, cx| {
if gallery.choice == Choice::System {
Theme::set_mode(window.appearance().into(), cx);
@@ -44,6 +51,7 @@ impl Gallery {
choice: Choice::Light,
focus,
scroll: ScrollHandle::new(),
narrow: narrow.map(px),
_appearance: appearance,
}
}
@@ -174,6 +182,7 @@ impl Gallery {
];
let colors = &cx.theme().colors;
div()
.flex_none()
.flex()
.gap_0p5()
.p_0p5()
@@ -198,6 +207,24 @@ impl Gallery {
}
}
impl Gallery {
/// Header and body; a padded card when narrow.
fn card(&self, header: impl IntoElement, body: AnyElement, cx: &App) -> AnyElement {
let content = div().child(header).child(body);
match self.narrow {
// Width plus padding and hairlines.
Some(width) => div()
.w(width + px(34.0))
.p_4()
.border_1()
.border_color(cx.theme().colors.border)
.child(content)
.into_any_element(),
None => content.into_any_element(),
}
}
}
impl Render for Gallery {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let page = &pages::ALL[self.page];
@@ -229,32 +256,38 @@ impl Render for Gallery {
.pt(px(56.0))
.pb(px(96.0))
.child(
div()
.flex()
.items_start()
.justify_between()
.gap_6()
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_size(theme.text_size(TextSize::Xxl))
.font_weight(FontWeight::SEMIBOLD)
.child(page.title),
)
.child(
div()
.text_size(theme.text_size(TextSize::Md))
.text_color(colors.fg_muted)
.child(page.summary),
),
)
.child(switcher),
)
.child(body),
self.card(
div()
.flex()
.flex_wrap()
.items_start()
.justify_between()
.gap_6()
.child(
div()
.flex_1()
.min_w(px(200.0))
.flex()
.flex_col()
.gap_2()
.child(
div()
.text_size(theme.text_size(TextSize::Xxl))
.font_weight(FontWeight::SEMIBOLD)
.child(page.title),
)
.child(
div()
.text_size(theme.text_size(TextSize::Md))
.text_color(colors.fg_muted)
.child(page.summary),
),
)
.child(switcher),
body,
cx,
),
),
),
)
}