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.
This commit is contained in:
ZacharyZhang-NY committed 2026-09-28 13:07:22 -04:00
1 parent 3be809a660
commit 81e73b79c1
126 files changed
+275 -276

No files matched your search

+3 -2
View File
@@ -22,6 +22,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- `cargo run --example gallery` opens the gallery. `-- --page <slug>` starts on a page.
- `cargo run --example gallery -- --capture <dir>` writes PNGs of every page, light and dark, top to bottom, then each page's scripted states, including windows the demos open. macOS only.
- `cargo run --example gallery -- --narrow 280 --capture <dir>` lays each page, header and body, at that width in a padded card and shoots it without its script, whose aims are measured at full width. Demos that set their own width run past the card.
- `cargo test --lib --features test-support`, `cargo clippy --all-targets --features test-support -- -D warnings`, `cargo fmt --check`. `scripts/check.sh` runs them with the house rules.
- `rm -rf target/debug/incremental` and `cargo clean -p ely-gpui-component` after each task item keep the disk lean. Cargo keeps every old build of the crate and its tests under a new hash; by T25 they had filled the disk.
- `scripts/icons.sh <lucide-name>...` adds icons. Then add a line to `IconName` in `src/primitives/icon.rs`.
@@ -111,7 +112,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- Trackpad momentum arrives as `TouchPhase::Moved` after `Ended`, so a gesture acts only between `Started` and `Ended` (`lists::SwipeableListItem`).
- Quartz scroll events carry no window. The harness hands them to the window itself, placed in window points (`Step::Swipe`).
- A chevron that opens and closes is `primitives::Disclosure`.
- `uniform_list` does not stretch its rows; each row sets `w_full`, or `min_w_full` when the same row also sits in a flex column, where it stretches (`tables::DataTable`).
- `uniform_list` does not stretch its rows; each row sets `w_full`, or `min_w_full` when the same row also sits in a flex column, where it stretches (`tables::DataTable`). gpui's `list` lays rows at their content's width too; `chat::MessageList` boxes each row at the list's width.
- Chapters import only earlier chapters, so a select of tree nodes lives in `lists` (`TreeSelect`) and reaches `forms::listing`.
- Script clicks aim at a probe's corner, so a scripted target must fit in the window; a tall probe puts its top off screen. The log names each probe's bounds as the script aims, and a probe around content with no width of its own measures smaller than what it draws, so a scripted demo sets its width.
- `scripts/check.sh` fails by its exit status; a pipe into `tail` hides it.
@@ -152,7 +153,7 @@ Ely GPUI Component. A component library for GPUI, in light and dark.
- A row that holds text beside fixed parts lets the text give way: the text takes `flex_1().min_w_0()` and an `Ellipsis`; counts, icons, dates and chevrons take `flex_none`. A label that must stay whole wraps in that block instead, as `forms::Checkbox`'s does.
- A mark over a text field reads the field at paint time, from a canvas laid over it (`collab::RemoteCursor`): by then `TextInput::bounds_for` holds this frame's layout, and a still page never renders again to catch up.
- A callback field holds a named `type` alias; clippy's type_complexity rejects `Option<Rc<dyn Fn(..)>>`. A chapter shares one alias per shape.
- A component that fills its container takes no width on its root: a block or a flex column stretches it. Percents fill badly in taffy 0.9. `w_full` in a flex column that a block measures by content keeps its content's width: taffy lays the column out once at unknown width and keeps that layout when the width comes out the same. `min_w_full` resolves against a block's outer width, padding and all (`forms::FormField`). In a flex row the host gives it `flex_1`; as a window's root it sits in a `size_full` box. Roots made before T30d still say `w_full`; T44 takes it off.
- A component that fills its container takes no width on its root: a block or a flex column stretches it. Percents fill badly in taffy 0.9. `w_full` in a flex column that a block measures by content keeps its content's width: taffy lays the column out once at unknown width and keeps that layout when the width comes out the same. `min_w_full` resolves against a block's outer width, padding and all (`forms::FormField`). In a flex row the host gives it `flex_1`; as a window's root it sits in a `size_full` box. A root centered under a cap keeps `w_full`, since `mx_auto` needs a width to fill (`layout::Container`). A row that hands a text field a `flex_1` share keeps `w_full` too: stretched without a width, taffy 0.9 can keep a pass that laid the share at no width, and the field draws nothing (`forms::Input`, `InputGroup`, `TagInput`).
- A field's `Highlight` sets color, wash, weight, slant and strike per span; measuring and painting share one set of runs, so wrapped heights match.
- A block editor keeps one undo for the document: text and structure together, as snapshots through `forms::History`; it takes Undo and Redo before its fields do.
- gpui calls focus listeners only while the window is active. A test that needs focus events activates its window with `window.activate_window()`.
+1 -1
View File
@@ -150,7 +150,7 @@ Per-component lines live in `tasks/`. Tags there:
- [x] T43a Tooling · Workbench — `tasks/ch31-43.md`
- [x] T43b Tooling · Inspection — `tasks/ch31-43.md`
- [x] T43c Tooling · Catalogs — `tasks/ch31-43.md`
- [ ] T44a Capture · Roots: roots still on `w_full` drop it; every section checked at 280px and in a padded card
- [ ] T44a Capture · Roots: roots still on `w_full` drop it, checked at 280px and in padded cards
- [ ] T44b Capture · Sections: every component's section shot on its own, light and dark, with a manifest the site reads
- [ ] T44c Capture · Motion: clips of each page's scripted states in motion, for the site
- [ ] T45 Website: `frontend/`, Vite 8, pnpm, light and dark: a home page in motion (three.js, smooth scroll), and a components page with every component, categories on the left and the chosen one's detail on the right, after astryx.atmeta.com
+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,
),
),
),
)
}
-2
View File
@@ -119,7 +119,6 @@ impl RenderOnce for FileChangeCard {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(colors.border)
@@ -270,7 +269,6 @@ impl RenderOnce for MultiFileDiffReview {
)
});
div()
.w_full()
.flex()
.flex_col()
.gap_2()
-2
View File
@@ -88,7 +88,6 @@ impl RenderOnce for HumanInputRequest {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.flex()
.flex_col()
.gap_3()
@@ -177,7 +176,6 @@ impl RenderOnce for CostBreakdown {
)
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -117,7 +117,6 @@ impl RenderOnce for SandboxStatus {
let live = matches!(self.state, SandboxState::Starting | SandboxState::Busy);
let color = self.state.color(&colors);
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -84,7 +84,6 @@ impl RenderOnce for MemoryPanel {
)
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -137,7 +137,6 @@ impl RenderOnce for PermissionPrompt {
})
};
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-2
View File
@@ -118,7 +118,6 @@ impl RenderOnce for AgentPlan {
.filter(|(_, state)| *state == StepState::Done)
.count();
div()
.w_full()
.flex()
.flex_col()
.gap_2()
@@ -284,7 +283,6 @@ impl RenderOnce for AgentProgress {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.flex()
.flex_col()
.gap_2()
-1
View File
@@ -112,7 +112,6 @@ impl RenderOnce for ToolRegistry {
let groups = grouped(&self.tools, &found(&self.tools, &self.query));
let empty = groups.is_empty();
div()
.w_full()
.flex()
.flex_col()
.gap_4()
-2
View File
@@ -109,7 +109,6 @@ impl RenderOnce for ToolCallCard {
.child(text)
};
div()
.w_full()
.rounded(theme.radius(Radius::Md))
.border_1()
.border_color(colors.border)
@@ -217,7 +216,6 @@ impl RenderOnce for ToolCallGroup {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.flex()
.flex_col()
.gap_2()
-2
View File
@@ -142,7 +142,6 @@ impl RenderOnce for AgendaView {
if owners.is_empty() {
return div()
.debug_selector(|| "agenda-none".into())
.w_full()
.py_6()
.flex()
.justify_center()
@@ -172,7 +171,6 @@ impl RenderOnce for AgendaView {
})
};
div()
.w_full()
.child(
sections
.selected(picked.read(cx).clone())
-1
View File
@@ -119,7 +119,6 @@ impl RenderOnce for AllDayRow {
});
div()
.relative()
.w_full()
.h(pad * 2.0 + pitch * lanes as f32 - gap)
.border_b_1()
.border_color(theme.colors.border)
-1
View File
@@ -93,7 +93,6 @@ impl RenderOnce for EventChip {
.flex()
.items_center()
.gap_1()
.w_full()
.h(theme.calendar().chip)
.px_1p5()
.rounded(round)
-1
View File
@@ -420,7 +420,6 @@ impl RenderOnce for CalendarMonthView {
.collect();
let title = shown.strftime("%B %Y").to_string();
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -173,7 +173,6 @@ impl RenderOnce for AttendeeList {
})
.collect();
div()
.w_full()
.flex()
.flex_col()
.gap_1()
-1
View File
@@ -300,7 +300,6 @@ impl RenderOnce for YearView {
})
.collect();
div()
.w_full()
.flex()
.flex_col()
.gap_4()
-2
View File
@@ -74,7 +74,6 @@ impl RenderOnce for QuoteReply {
let colors = theme.colors.clone();
let line = self.text.lines().next().unwrap_or_default().to_string();
div()
.w_full()
.flex()
.items_center()
.gap_2()
@@ -261,7 +260,6 @@ impl RenderOnce for MessageEditor {
self.on_cancel,
);
div()
.w_full()
.flex()
.flex_col()
.gap_2()
-2
View File
@@ -105,7 +105,6 @@ impl RenderOnce for SourceCard {
let open = self.on_open;
div()
.id(self.id.clone())
.w_full()
.p_2p5()
.rounded(theme.radius(Radius::Md))
.border_1()
@@ -222,7 +221,6 @@ impl RenderOnce for SourceList {
.take(3)
.collect();
div()
.w_full()
.flex()
.flex_col()
.gap_2()
-1
View File
@@ -94,7 +94,6 @@ impl RenderOnce for CodeBlock {
let lines = code.lines().count();
let toggle = open.clone();
div()
.w_full()
.flex()
.flex_col()
.rounded(theme.radius(Radius::Md))
-1
View File
@@ -179,7 +179,6 @@ impl RenderOnce for ConversationItem {
.id(self.id.clone())
.group(group.clone())
.track_focus(&focus)
.w_full()
.flex()
.items_center()
.gap_1()
+14 -3
View File
@@ -105,7 +105,16 @@ impl RenderOnce for MessageList {
div()
.relative()
.size_full()
.child(list(state, move |ix, window, cx| render(ix, window, cx)).size_full())
// gpui's list lays rows at content width.
.child(
list(state, move |ix, window, cx| {
div()
.w_full()
.child(render(ix, window, cx))
.into_any_element()
})
.size_full(),
)
.child(
canvas(
move |_, window, cx| {
@@ -234,8 +243,10 @@ impl RenderOnce for DateSeparator {
let theme = cx.theme();
let colors = theme.colors.clone();
let rule = || div().flex_1().h_px().bg(colors.border);
let name = day_name(self.day, self.today);
let shown = format!("date-separator-{name}");
div()
.w_full()
.debug_selector(move || shown)
.flex()
.items_center()
.gap_3()
@@ -245,7 +256,7 @@ impl RenderOnce for DateSeparator {
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle)
.child(day_name(self.day, self.today)),
.child(name),
)
.child(rule())
}
-3
View File
@@ -72,7 +72,6 @@ impl RenderOnce for ImageMessage {
let widest = theme.prose_width().to_pixels(window.rem_size());
let open = self.on_open;
div()
.w_full()
.flex()
.flex_col()
.gap_1()
@@ -235,7 +234,6 @@ impl RenderOnce for FileMessage {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.max_w(theme.prose_width())
.flex()
.items_center()
@@ -339,7 +337,6 @@ impl RenderOnce for LinkPreviewCard {
let open = self.on_open;
div()
.id(self.id.clone())
.w_full()
.max_w(theme.container_width(ContainerSize::Sm))
.flex()
.gap_3()
-2
View File
@@ -81,7 +81,6 @@ impl RenderOnce for ParameterPanel {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.flex()
.flex_col()
.gap_4()
@@ -274,7 +273,6 @@ impl RenderOnce for SystemPromptEditor {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
div()
.w_full()
.flex()
.flex_col()
.gap_2()
-1
View File
@@ -97,7 +97,6 @@ impl RenderOnce for AudioMessage {
let playing = self.playing;
let tall = theme.control_height(ControlSize::Md);
div()
.w_full()
.max_w(theme.prose_width())
.flex()
.items_center()
-2
View File
@@ -62,7 +62,6 @@ impl RenderOnce for ProjectList {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.flex()
.flex_col()
.gap_0p5()
@@ -163,7 +162,6 @@ impl RenderOnce for ProjectKnowledgePanel {
let used: u64 = self.files.iter().map(|(_, bytes)| bytes).sum();
let share = used as f32 / self.capacity as f32;
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-2
View File
@@ -164,7 +164,6 @@ impl RenderOnce for WebResultCard {
let open = self.on_open;
div()
.id(self.id.clone())
.w_full()
.flex()
.flex_col()
.gap_0p5()
@@ -273,7 +272,6 @@ impl RenderOnce for DocumentChunkPreview {
..HighlightStyle::default()
};
div()
.w_full()
.flex()
.flex_col()
.gap_1p5()
-3
View File
@@ -112,7 +112,6 @@ impl RenderOnce for RateLimitNotice {
None => countdown,
};
div()
.w_full()
.flex()
.items_center()
.gap_3()
@@ -198,7 +197,6 @@ impl RenderOnce for FeedbackForm {
let ready = !self.picked.is_empty() || !self.note.read(cx).text().trim().is_empty();
let (pick, send) = (self.on_pick, self.on_send);
div()
.w_full()
.flex()
.flex_col()
.gap_3()
@@ -356,7 +354,6 @@ impl RenderOnce for ThinkingBlock {
ThinkingDuration::new(self.took).into_any_element()
};
div()
.w_full()
.flex()
.flex_col()
.gap_2()
+28 -1
View File
@@ -13,7 +13,8 @@ mod history;
mod welcome;
use super::{
ImageMessage, MessageAvatar, MessageBubble, MessageEditor, MessageList, Role, ThinkingBlock,
DateSeparator, ImageMessage, MessageAvatar, MessageBubble, MessageEditor, MessageList, Role,
ThinkingBlock,
};
use crate::{forms, forms::TextInput, theme::Theme};
@@ -361,3 +362,29 @@ fn the_reasoning_opens_from_the_keyboard(cx: &mut TestAppContext) {
"Tab and Space open it: {closed:?} to {open:?}"
);
}
struct Dated;
impl Render for Dated {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let day = jiff::civil::date(2026, 9, 28);
div()
.size_full()
.child(MessageList::new("dated", 1, move |_, _, _| {
DateSeparator::new(day, day).into_any_element()
}))
}
}
/// A row no wider than its words still spans the list.
#[gpui::test]
fn a_narrow_row_spans_the_list(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (_, cx) = cx.add_window_view(|_, _| Dated);
settle(cx);
let row = cx
.debug_bounds("date-separator-Today")
.expect("the separator draws");
let width = cx.update(|window, _| window.viewport_size().width);
assert_eq!(row.size.width, width);
}
-2
View File
@@ -180,7 +180,6 @@ impl RenderOnce for FollowUpSuggestions {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.flex()
.flex_col()
.children(
@@ -253,7 +252,6 @@ impl RenderOnce for CapabilityCards {
let theme = cx.theme();
let colors = theme.colors.clone();
div()
.w_full()
.grid()
.grid_cols(3)
.gap_3()
+17 -1
View File
@@ -12,12 +12,28 @@ fn open(
forms::bind_keys(cx);
crate::documents::bind_keys(cx);
});
let (editor, cx) = cx.add_window_view(|window, cx| BlockEditor::new(blocks, window, cx));
let (full, cx) =
cx.add_window_view(|window, cx| Full(cx.new(|cx| BlockEditor::new(blocks, window, cx))));
let editor = full.read_with(cx, |full, _| full.0.clone());
cx.update(|window, _| window.activate_window());
settle(cx);
(editor, cx)
}
/// The editor as a window's content, in the box that fills the window.
struct Full(Entity<BlockEditor>);
impl gpui::Render for Full {
fn render(
&mut self,
_: &mut gpui::Window,
_: &mut gpui::Context<Self>,
) -> impl gpui::IntoElement {
use gpui::{ParentElement, Styled};
gpui::div().size_full().child(self.0.clone())
}
}
fn settle(cx: &mut VisualTestContext) {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
-1
View File
@@ -253,7 +253,6 @@ impl Render for BlockEditor {
let start = cx.entity();
div()
.id(id.clone())
.w_full()
.flex()
.flex_col()
.capture_action(cx.listener(|editor, _: &Undo, window, cx| {
-1
View File
@@ -108,7 +108,6 @@ impl RenderOnce for PageCover {
.id(self.id.clone())
.group(group.clone())
.relative()
.w_full()
.h(theme.page_cover())
.child(
Image::new((self.id.clone(), "picture"), source(&self.source))
+16 -20
View File
@@ -208,27 +208,23 @@ impl RenderOnce for ReadingProgress {
let theme = cx.theme();
let (track, bar) = (theme.colors.border, theme.colors.accent);
let scroll = self.scroll;
div()
.w_full()
.h(theme.progress_thickness())
.bg(track)
.child(
canvas(
|_, _, _| {},
move |bounds, _, window, _| {
let share = read_share(
f32::from(scroll.offset().y),
f32::from(scroll.max_offset().height),
);
let filled = Bounds::new(
bounds.origin,
size(bounds.size.width * share, bounds.size.height),
);
window.paint_quad(fill(filled, bar));
},
)
.size_full(),
div().h(theme.progress_thickness()).bg(track).child(
canvas(
|_, _, _| {},
move |bounds, _, window, _| {
let share = read_share(
f32::from(scroll.offset().y),
f32::from(scroll.max_offset().height),
);
let filled = Bounds::new(
bounds.origin,
size(bounds.size.width * share, bounds.size.height),
);
window.paint_quad(fill(filled, bar));
},
)
.size_full(),
)
}
}
-1
View File
@@ -166,7 +166,6 @@ impl RenderOnce for Banner {
.items_center()
.justify_center()
.gap_2()
.w_full()
.min_h(theme.control_height(ControlSize::Lg))
.py_1()
.px_10()
-1
View File
@@ -212,7 +212,6 @@ impl RenderOnce for DuplicateFinder {
});
div()
.debug_selector(|| "duplicate-finder".into())
.w_full()
.flex()
.flex_col()
.gap_4()
-1
View File
@@ -278,7 +278,6 @@ impl RenderOnce for FileExplorer {
};
div()
.debug_selector(|| "file-explorer".into())
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -177,7 +177,6 @@ impl RenderOnce for FileGrid {
.id(self.id)
.debug_selector(|| "file-grid".into())
.relative()
.w_full()
.flex()
.flex_wrap()
.gap(gap)
-1
View File
@@ -174,7 +174,6 @@ impl RenderOnce for FileOperationProgress {
);
div()
.debug_selector(|| "file-operation".into())
.w_full()
.flex()
.flex_col()
.gap_2()
-1
View File
@@ -122,7 +122,6 @@ impl RenderOnce for FilePreview {
};
div()
.debug_selector(|| "file-preview".into())
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -140,7 +140,6 @@ impl RenderOnce for RecentFiles {
});
div()
.debug_selector(|| "recent-files".into())
.w_full()
.flex()
.flex_col()
.gap_4()
-1
View File
@@ -122,7 +122,6 @@ impl RenderOnce for FileSearch {
};
div()
.debug_selector(|| "file-search".into())
.w_full()
.flex()
.flex_col()
.gap_3()
-2
View File
@@ -80,7 +80,6 @@ impl RenderOnce for TransferQueue {
let theme = cx.theme();
div()
.debug_selector(|| "transfer-queue".into())
.w_full()
.flex()
.flex_col()
.gap_1()
@@ -192,7 +191,6 @@ impl RenderOnce for DownloadManager {
});
div()
.debug_selector(|| "download-manager".into())
.w_full()
.flex()
.flex_col()
.gap_1()
+5 -8
View File
@@ -138,13 +138,9 @@ impl RenderOnce for FontPicker {
if let Some(on_change) = self.on_change {
combobox = combobox.on_change(move |family, window, cx| on_change(family, window, cx));
}
div()
.flex()
.flex_col()
.gap_2()
.w_full()
.child(combobox)
.when_some(self.selected, |picker, family| {
div().flex().flex_col().gap_2().child(combobox).when_some(
self.selected,
|picker, family| {
picker.child(
div()
.font_family(family)
@@ -152,7 +148,8 @@ impl RenderOnce for FontPicker {
.text_color(theme.colors.fg_muted)
.child("The quick brown fox jumps over the lazy dog"),
)
})
},
)
}
}
-2
View File
@@ -173,7 +173,6 @@ impl RenderOnce for CodeInput {
"a code input is one line; JsonInput takes several"
);
div()
.w_full()
.font_family(cx.theme().mono_family.clone())
.child(Input::new(&self.state).size(self.size))
}
@@ -205,7 +204,6 @@ impl RenderOnce for JsonInput {
.flex()
.flex_col()
.gap_1()
.w_full()
.child(
div()
.font_family(theme.mono_family.clone())
-1
View File
@@ -275,7 +275,6 @@ impl RenderOnce for GradientEditor {
.flex()
.flex_col()
.gap_2()
.w_full()
.child(
div()
.flex()
-1
View File
@@ -375,7 +375,6 @@ impl RenderOnce for ColorPicker {
.flex()
.flex_col()
.gap_3()
.w_full()
.px(knob * 0.5)
.pt(knob * 0.5)
.child(plane)
-1
View File
@@ -214,7 +214,6 @@ impl RenderOnce for Combobox {
div()
.id(self.id.clone())
.relative()
.w_full()
.capture_action(move |_: &Up, _, cx| {
if open {
cx.stop_propagation();
-1
View File
@@ -276,7 +276,6 @@ impl RenderOnce for ExpressionInput {
.flex()
.flex_col()
.gap_1()
.w_full()
.child(
div().font_family(mono).child(
Input::new(&self.state)
-1
View File
@@ -58,7 +58,6 @@ impl RenderOnce for HotkeyInput {
.items_center()
.justify_between()
.gap_2()
.w_full()
.h(theme.control_height(ControlSize::Md))
.px(theme.control_padding(ControlSize::Md))
.rounded(theme.radius(Radius::Md))
-1
View File
@@ -129,7 +129,6 @@ impl RenderOnce for InlineEdit {
let (escape, enter) = (state.clone(), state.clone());
return div()
.id(self.id)
.w_full()
.on_key_down(move |event, window, cx| {
if event.keystroke.key == "escape" {
cx.stop_propagation();
-1
View File
@@ -270,7 +270,6 @@ impl RenderOnce for PhoneInput {
);
div()
.relative()
.w_full()
.child(MaskedInput::new(self.id.clone(), &self.state, mask).prefix(code))
.child(
canvas(
-1
View File
@@ -153,7 +153,6 @@ impl RenderOnce for MultiSelect {
.flex_wrap()
.items_center()
.gap_1()
.w_full()
.min_h(theme.control_height(self.size))
.pl_1()
.pr(theme.control_padding(self.size))
-1
View File
@@ -158,7 +158,6 @@ impl RenderOnce for FieldArray {
.flex()
.flex_col()
.gap_2()
.w_full()
.children(lines)
.child(
div().flex().child(
-1
View File
@@ -136,7 +136,6 @@ impl RenderOnce for RegexInput {
.flex()
.flex_col()
.gap_1()
.w_full()
.child(
div().font_family(mono).child(
Input::new(&self.state)
-1
View File
@@ -81,7 +81,6 @@ impl RenderOnce for SearchInput {
});
div()
.relative()
.w_full()
.child(
Input::new(&self.state)
.size(self.size)
-1
View File
@@ -260,7 +260,6 @@ impl RenderOnce for SignaturePad {
.flex_col()
.items_end()
.gap_1()
.w_full()
.child(pad)
.child(
Button::new((self.id, "clear"), "Clear")
-3
View File
@@ -55,7 +55,6 @@ impl RenderOnce for Form {
.flex()
.flex_col()
.gap_8()
.w_full()
.when_some(self.on_submit, |form, submit| {
form.capture_action(move |_: &Submit, window, cx| {
cx.stop_propagation();
@@ -102,7 +101,6 @@ impl RenderOnce for FormSection {
.flex()
.flex_col()
.gap_4()
.w_full()
.child(
div()
.flex()
@@ -304,7 +302,6 @@ impl RenderOnce for InlineForm {
.flex_wrap()
.items_end()
.gap_3()
.w_full()
.children(self.children)
}
}
+20 -16
View File
@@ -1,6 +1,6 @@
use gpui::{
Context, Hsla, IntoElement, Modifiers, MouseButton, Render, TestAppContext, Window, point, px,
rgb, rgba,
Context, Hsla, IntoElement, Modifiers, MouseButton, ParentElement, Render, Styled,
TestAppContext, Window, div, point, px, rgb, rgba,
};
use super::setup;
@@ -13,14 +13,16 @@ struct Picked {
impl Render for Picked {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
ColorPicker::new("picked", self.color)
.opaque()
.on_change(move |color, _, cx| {
view.update(cx, |view, cx| {
view.color = color;
cx.notify();
})
})
div()
.size_full()
.child(ColorPicker::new("picked", self.color).opaque().on_change(
move |color, _, cx| {
view.update(cx, |view, cx| {
view.color = color;
cx.notify();
})
},
))
}
}
@@ -41,12 +43,14 @@ struct Gradient {
impl Render for Gradient {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
GradientEditor::new("gradient", self.stops.clone()).on_change(move |next, _, cx| {
view.update(cx, |view, cx| {
view.stops = next.to_vec();
cx.notify();
})
})
div().size_full().child(
GradientEditor::new("gradient", self.stops.clone()).on_change(move |next, _, cx| {
view.update(cx, |view, cx| {
view.stops = next.to_vec();
cx.notify();
})
}),
)
}
}
+10 -6
View File
@@ -48,12 +48,16 @@ struct Pad {
impl Render for Pad {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
SignaturePad::new("pad", self.strokes.clone()).on_change(move |next, _, cx| {
view.update(cx, |view, cx| {
view.strokes = next.to_vec();
cx.notify();
})
})
div()
.size_full()
.child(
SignaturePad::new("pad", self.strokes.clone()).on_change(move |next, _, cx| {
view.update(cx, |view, cx| {
view.strokes = next.to_vec();
cx.notify();
})
}),
)
}
}
-1
View File
@@ -358,7 +358,6 @@ impl Render for TextInput {
.id("ely-text-input")
.key_context(CONTEXT)
.track_focus(&self.focus)
.w_full()
.cursor(if self.disabled {
CursorStyle::OperationNotAllowed
} else {
-1
View File
@@ -181,7 +181,6 @@ impl RenderOnce for TransferList {
div()
.flex()
.gap_3()
.w_full()
.child(side("left", available_title, available, left, cx))
.child(buttons)
.child(side("right", chosen_title, picked, right, cx))
-1
View File
@@ -152,7 +152,6 @@ impl RenderOnce for AudioGenerationPlayer {
}
};
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -80,7 +80,6 @@ impl RenderOnce for MaskBrush {
})
};
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -168,7 +168,6 @@ impl RenderOnce for ABCompareView {
.collect(),
};
div()
.w_full()
.flex()
.flex_col()
.gap_3()
+1 -1
View File
@@ -256,7 +256,7 @@ impl RenderOnce for ModelDownloadManager {
)
.children(bar)
});
div().w_full().flex().flex_col().children(rows)
div().flex().flex_col().children(rows)
}
}
+1 -7
View File
@@ -313,12 +313,6 @@ impl RenderOnce for EmbeddingVisualizer {
cx.notify();
})
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.child(legend)
.child(area)
div().flex().flex_col().gap_3().child(legend).child(area)
}
}
+1 -1
View File
@@ -207,6 +207,6 @@ impl RenderOnce for GenerationGrid {
.children((0..short).map(|_| div().flex_1())),
);
}
div().w_full().flex().flex_col().gap_2().children(rows)
div().flex().flex_col().gap_2().children(rows)
}
}
+6 -4
View File
@@ -45,8 +45,11 @@ impl HardwareMonitor {
impl RenderOnce for HardwareMonitor {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let spark = cx.theme().spark_size();
div().w_full().flex().flex_col().gap_3().children(
self.readings.into_iter().enumerate().map(|(ix, reading)| {
div()
.flex()
.flex_col()
.gap_3()
.children(self.readings.into_iter().enumerate().map(|(ix, reading)| {
div()
.flex()
.items_end()
@@ -71,7 +74,6 @@ impl RenderOnce for HardwareMonitor {
.child(Sparkline::new(reading.history).area()),
)
})
}),
)
}))
}
}
-2
View File
@@ -95,7 +95,6 @@ impl RenderOnce for ModelCard {
})
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
@@ -260,7 +259,6 @@ impl RenderOnce for ModelStatus {
_ => None,
};
div()
.w_full()
.flex()
.flex_col()
.gap_2()
-1
View File
@@ -154,7 +154,6 @@ impl RenderOnce for PromptPlayground {
)
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -199,7 +199,6 @@ impl RenderOnce for PromptEnhancer {
),
);
div()
.w_full()
.flex()
.flex_col()
.child(Input::new(&self.field))
-1
View File
@@ -76,7 +76,6 @@ impl RenderOnce for AspectRatioPicker {
let colors = theme.colors.clone();
let glyph = theme.icon_size(IconSize::Md);
div()
.w_full()
.flex()
.flex_wrap()
.gap_2()
-1
View File
@@ -75,7 +75,6 @@ impl RenderOnce for StylePresetPicker {
let colors = theme.colors.clone();
let side = theme.generative().preset;
div()
.w_full()
.flex()
.flex_wrap()
.gap_3()
-1
View File
@@ -185,7 +185,6 @@ impl RenderOnce for FineTuneJobCard {
.h(theme.generative().loss)
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -120,7 +120,6 @@ impl RenderOnce for VariationPicker {
})
};
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-2
View File
@@ -84,7 +84,6 @@ impl RenderOnce for PromptDiff {
Some((range, style))
});
div()
.w_full()
.text_size(theme.text_size(TextSize::Sm))
.text_color(colors.fg)
.child(StyledText::new(text).with_highlights(styles))
@@ -251,7 +250,6 @@ impl RenderOnce for PromptVersionHistory {
},
);
div()
.w_full()
.flex()
.flex_wrap()
.items_start()
-1
View File
@@ -287,7 +287,6 @@ impl RenderOnce for VideoGenerationTimeline {
})
});
div()
.w_full()
.flex()
.flex_col()
.gap_2()
+1 -1
View File
@@ -224,6 +224,6 @@ impl RenderOnce for TTSVoicePicker {
.children(voice.tags.into_iter().map(Badge::new)),
)
});
div().w_full().flex().flex_col().gap_0p5().children(rows)
div().flex().flex_col().gap_0p5().children(rows)
}
}
-1
View File
@@ -87,7 +87,6 @@ impl RenderOnce for SimpleGrid {
let columns = columns_for(*width.read(cx), self.min, self.gap);
self.base
.relative()
.w_full()
.grid()
.grid_cols(columns)
.gap(self.gap)
+1 -1
View File
@@ -115,6 +115,6 @@ impl RenderOnce for Sections {
}),
)
});
div().w_full().flex().flex_col().gap_4().children(sections)
div().flex().flex_col().gap_4().children(sections)
}
}
+7 -10
View File
@@ -2,7 +2,7 @@ use std::rc::Rc;
use gpui::{
App, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString,
Styled, Window, div,
Window, div,
};
use super::labels::hue_dot;
@@ -173,14 +173,11 @@ impl RenderOnce for MailboxList {
sections.section(title.clone(), rows)
},
);
div()
.debug_selector(|| "mailbox-list".into())
.w_full()
.child(
sections
.selected(self.open)
.on_select(open.clone())
.on_activate(open),
)
div().debug_selector(|| "mailbox-list".into()).child(
sections
.selected(self.open)
.on_select(open.clone())
.on_activate(open),
)
}
}
-1
View File
@@ -330,7 +330,6 @@ impl RenderOnce for MailComposer {
});
div()
.debug_selector(|| "mail-composer".into())
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -240,7 +240,6 @@ impl RenderOnce for LabelPicker {
let (up, down, chosen) = (finding.clone(), finding.clone(), finding);
div()
.id(self.id.clone())
.w_full()
.flex()
.flex_col()
.gap_2()
-2
View File
@@ -290,7 +290,6 @@ impl RenderOnce for MailList {
if self.mails.is_empty() {
return div()
.debug_selector(|| "mail-none".into())
.w_full()
.px_3()
.py_4()
.text_size(theme.text_size(TextSize::Sm))
@@ -320,7 +319,6 @@ impl RenderOnce for MailList {
let (id, on_select) = (self.id.clone(), self.on_select);
div()
.debug_selector(|| "mail-list".into())
.w_full()
.child(
list.selected(self.selected)
.on_change(move |keys, window, cx| {
-1
View File
@@ -344,7 +344,6 @@ impl RenderOnce for MailReader {
let theme = cx.theme();
div()
.debug_selector(|| "mail-reader".into())
.w_full()
.flex()
.flex_col()
.gap_4()
-1
View File
@@ -124,7 +124,6 @@ impl RenderOnce for SignatureEditor {
let words = input.read(cx).text().to_string();
let theme = cx.theme();
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -199,7 +199,6 @@ impl RenderOnce for MailThreadView {
);
div()
.debug_selector(|| "mail-thread".into())
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -428,7 +428,6 @@ impl RenderOnce for ImageAnnotator {
})
});
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-2
View File
@@ -145,7 +145,6 @@ impl RenderOnce for AudioWaveform {
.id(self.id.clone())
.debug_selector(|| "audio-waveform".into())
.relative()
.w_full()
.h(sizes.waveform)
.border_1()
.border_color(transparent_black())
@@ -226,7 +225,6 @@ impl RenderOnce for AudioSpectrum {
let (color, round) = (theme.colors.fg_muted, theme.radius(Radius::Sm));
div()
.debug_selector(|| "audio-spectrum".into())
.w_full()
.h(theme.media().waveform)
.flex()
.items_end()
-1
View File
@@ -92,7 +92,6 @@ impl RenderOnce for VolumeControl {
None => slider,
};
div()
.w_full()
.flex()
.items_center()
.gap_2()
-1
View File
@@ -200,7 +200,6 @@ impl RenderOnce for ImageCropper {
div()
.id(self.id.clone())
.relative()
.w_full()
.p(side / 2.0)
.border_1()
.border_color(transparent_black())
-1
View File
@@ -251,7 +251,6 @@ impl RenderOnce for AudioPlayer {
};
let left = format!("-{}", clock(self.length - self.at));
div()
.w_full()
.flex()
.flex_col()
.gap_3()
-1
View File
@@ -255,7 +255,6 @@ impl RenderOnce for MicLevelMeter {
let (unlit, round) = (colors.border, theme.radius(Radius::Sm));
div()
.debug_selector(|| "mic-level".into())
.w_full()
.h(theme.meter_track())
.flex()
.gap_0p5()
-1
View File
@@ -264,7 +264,6 @@ impl RenderOnce for Scrubber {
div()
.id(self.id.clone())
.debug_selector(|| "scrubber".into())
.w_full()
.px(thumb / 2.0)
.rounded(theme.radius(Radius::Sm))
.border_1()
-1
View File
@@ -251,7 +251,6 @@ impl RenderOnce for VideoThumbnailStrip {
let (pressed_strip, moved_strip) = (strip.clone(), strip.clone());
div()
.id(self.id.clone())
.w_full()
.px(handle / 2.0)
.border_1()
.border_color(transparent_black())
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more