Capture · Roots: the roots round 2 missed

A sweep that read every render found 28 roots still on w_full,
reached by 64 components: the field button under Select and every
picker, the checked and mention fields, the slider track,
NumberInput, Steps, the state views, the agent previews, and every
chart and finance frame. All drop it. LivePreview's size_full becomes
h_full, since its card no longer fills.

Every host of those roots was listed and read, 231 in all. Five
tests that mounted a picker as the window root sit in a size_full
box, the index manager gives its method select a flex_1 box, and the
notification center and the gallery's state frames stack their state
views in a column so they stretch.

Each dropped root carries a debug selector, and six chapter tests
mount 43 components in a padded card whose column is measured by
content, through layout::tests::narrow_width: each root must take the
full 240px. With w_full back on the roots, all six fail.

AGENTS.md names the roots that keep w_full and why: picture frames
that keep a ratio, and rows that hand a text field its share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-28 19:34:01 -04:00
1 parent ca5c967f17
commit e482bac865
43 files changed
+465 -108

No files matched your search

+1 -1
View File
@@ -153,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. A root centered under a cap keeps `w_full`, since `mx_auto` needs a width to fill (`layout::Container`); so does one that keeps a ratio, since taffy 0.9 follows a ratio from a set width, not a stretch (`layout::AspectRatio`). 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 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`); so does one that keeps a ratio, since taffy 0.9 follows a ratio from a set width, not a stretch (`layout::AspectRatio`, and `primitives::framed` under VideoPlayer, CameraPreview, ModelViewer, ParticipantTile and InpaintCanvas). A row that hands the text field element itself 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 row around one of those, as `NumberInput`'s is, takes none. `Button::full_width` is its host asking, since a button takes no style. Each chapter's test holds its filling roots at the full width of such a column through `layout::tests::narrow_width`, which reads a root by its `debug_selector`.
- 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
View File
@@ -473,3 +473,4 @@ Per-component lines live in `tasks/`. Tags there:
| T43c | 1 | FAIL | the audit left fg_subtle on active and every syntax color unread, both under 4.5:1; the viewer left out avatar and container sizes while claiming every token |
| T43c | 2 | PASS | the AGENTS.md decision narrowed to the pairs the audit holds, in the pass commit |
| T44a | 1 | FAIL | twelve roots in nine components still said w_full: a root held in a variable, a match arm or an animation passed the sweep |
| T44a | 2 | FAIL | roots built in render(mut self), returned by shared helpers or passed through them still said w_full: fields, charts, state views, Steps, previews; five measured at content width in a padded card |
+1 -1
View File
@@ -358,7 +358,7 @@ pub fn empties(cx: &mut App) -> impl IntoElement + use<> {
.min_w_0()
.h_80()
.flex()
.items_center()
.flex_col()
.justify_center()
.rounded(theme.radius(Radius::Lg))
.border_1()
+1 -1
View File
@@ -26,7 +26,7 @@ fn frame(cx: &App) -> Div {
.min_w_0()
.h_80()
.flex()
.items_center()
.flex_col()
.justify_center()
.rounded(theme.radius(Radius::Lg))
.border_1()
+2 -2
View File
@@ -56,7 +56,7 @@ fn address_bar(
fn card(cx: &App) -> Div {
let theme = cx.theme();
div()
.w_full()
.debug_selector(|| "preview-root".into())
.flex()
.flex_col()
.rounded(theme.radius(Radius::Lg))
@@ -395,7 +395,7 @@ impl RenderOnce for LivePreview {
.into_any_element()
});
card(cx)
.size_full()
.h_full()
.child(address_bar(&self.id, self.url, self.updating, reload, cx))
.child(
div()
+23 -1
View File
@@ -7,8 +7,9 @@ use gpui::{
use super::{press, settle};
use crate::{
agent::{ArtifactPanel, BrowserPreview, ComputerUseViewer},
agent::{ArtifactPanel, BrowserPreview, ComputerUseViewer, LivePreview},
forms,
layout::tests::narrow_width,
primitives::{FocusNext, IconName},
theme::{ActiveTheme, AvatarSize, Theme},
};
@@ -288,3 +289,24 @@ fn a_screen_takes_its_pictures_shape(cx: &mut TestAppContext) {
height.get()
);
}
#[gpui::test]
fn previews_fill_a_column_their_block_measures_by_content(cx: &mut TestAppContext) {
setup(cx);
let widths = [
narrow_width(cx, "preview-root", |_, _| {
BrowserPreview::new("browser", "localhost", ["a.png"], 0).into_any_element()
}),
narrow_width(cx, "preview-root", |_, _| {
ComputerUseViewer::new("screen", "a.png").into_any_element()
}),
narrow_width(cx, "preview-root", |_, _| {
LivePreview::new("live", "localhost", div()).into_any_element()
}),
];
assert_eq!(
widths,
[px(240.0); 3],
"each preview spans the card inside its padding"
);
}
+5 -3
View File
@@ -1,8 +1,8 @@
use std::rc::Rc;
use gpui::{
App, Div, ElementId, Entity, Hsla, IntoElement, ParentElement, Pixels, Refineable, RenderOnce,
StyleRefinement, Styled, Window, canvas, div, fill,
App, Div, ElementId, Entity, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
Refineable, RenderOnce, StyleRefinement, Styled, Window, canvas, div, fill,
};
use jiff::{Span, civil::Date};
@@ -157,7 +157,9 @@ impl RenderOnce for CalendarHeatmap {
);
let hit = days.iter().map(|(rect, _)| *rect).collect::<Vec<_>>();
let legend = key(shades, ("Less", "More"), cx);
let mut root = div().w_full().h(sizes.height * 0.5);
let mut root = div()
.debug_selector(|| "chart-root".into())
.h(sizes.height * 0.5);
root.style().refine(self.base.style());
let chart = tracked(
div().relative().flex_1().min_h_0(),
+5 -3
View File
@@ -1,8 +1,8 @@
use std::{collections::HashSet, rc::Rc};
use gpui::{
App, Div, ElementId, IntoElement, Refineable, RenderOnce, SharedString, StyleRefinement,
Styled, Window, div,
App, Div, ElementId, InteractiveElement, IntoElement, Refineable, RenderOnce, SharedString,
StyleRefinement, Styled, Window, div,
};
use super::{
@@ -131,7 +131,9 @@ impl Chart {
/// Renders a plot at the theme's chart height unless the owner sizes it.
pub(crate) fn sized(base: &mut Div, plotted: Plot, window: &mut Window, cx: &mut App) -> Div {
let mut frame = div().w_full().h(cx.theme().chart().height);
let mut frame = div()
.debug_selector(|| "chart-root".into())
.h(cx.theme().chart().height);
frame.style().refine(base.style());
plot(plotted, frame, window, cx)
}
+4 -1
View File
@@ -342,7 +342,10 @@ impl RenderOnce for SankeyChart {
let palette = colors.clone();
let (hit_nodes, hit_ribbons) = (nodes, ribbons);
let (moved, left) = (flows.clone(), flows.clone());
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
root.id((self.id.clone(), "sankey"))
.on_mouse_move(move |event: &MouseMoveEvent, _, cx| {
+4 -1
View File
@@ -184,7 +184,10 @@ impl RenderOnce for FunnelChart {
frame.h / self.stages.len().max(1) as f32,
);
let count = self.stages.len();
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
root.id((self.id.clone(), "funnel"))
.on_mouse_move(move |event: &MouseMoveEvent, _, cx| {
+4 -1
View File
@@ -287,7 +287,10 @@ impl RenderOnce for NetworkGraph {
graph.clone(),
);
let press_hit = hit.clone();
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
root.id((self.id.clone(), "network"))
.on_mouse_down(MouseButton::Left, move |event: &MouseDownEvent, _, cx| {
+6 -3
View File
@@ -1,8 +1,8 @@
use std::rc::Rc;
use gpui::{
App, Div, ElementId, Entity, IntoElement, ParentElement, PathBuilder, Pixels, Refineable,
RenderOnce, SharedString, StyleRefinement, Styled, Window, canvas, div,
App, Div, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, PathBuilder,
Pixels, Refineable, RenderOnce, SharedString, StyleRefinement, Styled, Window, canvas, div,
};
use super::{
@@ -215,7 +215,10 @@ impl RenderOnce for ParallelCoordinates {
lines.clone(),
);
let reach = pixels(sizes.inset);
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
tracked(
root,
+1 -1
View File
@@ -302,7 +302,7 @@ impl RenderOnce for PieChart {
|legend, (ix, (name, _))| legend.entry(tint(&colors, ix), name.clone(), false),
);
let (moved, left, measured, hit) = (ring.clone(), ring.clone(), ring.clone(), arcs.clone());
let mut frame = div().w_full().h(sizes.height);
let mut frame = div().debug_selector(|| "chart-root".into()).h(sizes.height);
frame.style().refine(self.base.style());
frame.flex().flex_col().gap_2().child(legend).child(
div()
+1 -1
View File
@@ -245,7 +245,7 @@ impl RenderOnce for RadarChart {
let (stroke, hairline) = (sizes.stroke.to_pixels(rem), sizes.hairline);
let palette = colors.clone();
let (moved, left, measured) = (radar.clone(), radar.clone(), radar.clone());
let mut frame = div().w_full().h(sizes.height);
let mut frame = div().debug_selector(|| "chart-root".into()).h(sizes.height);
frame.style().refine(self.base.style());
frame.flex().flex_col().gap_2().children(legend).child(
div()
+7 -3
View File
@@ -1,8 +1,9 @@
use std::time::Instant;
use gpui::{
App, Bounds, ContentMask, Div, ElementId, Entity, IntoElement, ParentElement, PathBuilder,
Pixels, Refineable, RenderOnce, StyleRefinement, Styled, Window, canvas, div, fill, size,
App, Bounds, ContentMask, Div, ElementId, Entity, InteractiveElement, IntoElement,
ParentElement, PathBuilder, Pixels, Refineable, RenderOnce, StyleRefinement, Styled, Window,
canvas, div, fill, size,
};
use super::{
@@ -128,7 +129,10 @@ impl RenderOnce for RealtimeChart {
pixels(sizes.inset),
);
let measured = live.clone();
let mut chart = div().relative().w_full().h(sizes.height * 0.6);
let mut chart = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height * 0.6);
chart.style().refine(self.base.style());
chart
.child(
+4 -1
View File
@@ -222,7 +222,10 @@ impl RenderOnce for ChordDiagram {
})
.collect();
let (palette, arcs, hit) = (colors.clone(), groups.clone(), groups);
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
tracked(
root,
+4 -3
View File
@@ -1,6 +1,7 @@
use gpui::{
App, Bounds, Div, ElementId, Entity, IntoElement, ParentElement, PathBuilder, Pixels,
Refineable, RenderOnce, SharedString, StyleRefinement, Styled, Window, canvas, div, fill, size,
App, Bounds, Div, ElementId, Entity, InteractiveElement, IntoElement, ParentElement,
PathBuilder, Pixels, Refineable, RenderOnce, SharedString, StyleRefinement, Styled, Window,
canvas, div, fill, size,
};
use jiff::{
Span,
@@ -267,8 +268,8 @@ impl RenderOnce for GanttChart {
);
let painted = bars.clone();
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.w_full()
.h(sizes.foot + theme.table_row(Density::Compact) * count as f32);
root.style().refine(self.base.style());
tracked(
+4 -1
View File
@@ -187,7 +187,10 @@ impl RenderOnce for Sunburst {
);
let (moved, left, measured, hit) = (ring.clone(), ring.clone(), ring.clone(), arcs.clone());
let painted = arcs.clone();
let mut frame = div().relative().w_full().h(sizes.height);
let mut frame = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
frame.style().refine(self.base.style());
frame
.id((self.id.clone(), "ring"))
+1 -1
View File
@@ -193,8 +193,8 @@ impl RenderOnce for BulletChart {
colors.clone(),
);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.w_full()
.h(theme.table_row(Density::Comfortable) * count as f32);
root.style().refine(self.base.style());
tracked(
+126 -4
View File
@@ -10,11 +10,12 @@ use gpui::{
use jiff::civil::date;
use super::{
BarChart, Bullet, BulletChart, CalendarHeatmap, ChartLegend, ChordDiagram, FunnelChart,
GanttChart, HeatmapChart, LineChart, NetworkGraph, ParallelCoordinates, ProgressChart,
RadarChart, SankeyChart, Series, Task, Treemap,
AreaChart, BarChart, BoxPlot, Bullet, BulletChart, CalendarHeatmap, ChartLegend, ChordDiagram,
FunnelChart, GanttChart, HeatmapChart, Histogram, LineChart, NetworkGraph, ParallelCoordinates,
PieChart, Points, ProgressChart, RadarChart, RealtimeChart, SankeyChart, ScatterChart, Series,
Slice, Sunburst, Task, Treemap, ViolinPlot, WaterfallChart,
};
use crate::theme::Theme;
use crate::{layout::tests::narrow_width, theme::Theme};
#[gpui::test]
fn a_chart_exports_as_svg_and_csv(cx: &mut TestAppContext) {
@@ -208,3 +209,124 @@ fn tab_reaches_a_legend_entry_and_enter_toggles_it(cx: &mut TestAppContext) {
settle(cx);
assert_eq!(*heard.borrow(), ["Held out"]);
}
#[gpui::test]
fn every_chart_fills_a_column_its_block_measures_by_content(cx: &mut TestAppContext) {
cx.update(Theme::init);
let two = || Series::new("s", [1.0, 2.0]);
let pairs = [vec![0.0, 1.0], vec![1.0, 0.0]];
let widths = [
narrow_width(cx, "chart-root", move |_, _| {
LineChart::new("line", ["a", "b"])
.series(two())
.into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
AreaChart::new("area", ["a", "b"])
.series(two())
.into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
BarChart::new("bar", ["a", "b"])
.series(two())
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
let points = Points::new("p", [(0.0, 1.0), (1.0, 2.0)]);
ScatterChart::new("scatter")
.points(points)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
Histogram::new("histogram", [1.0, 2.0, 2.0, 3.0]).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
WaterfallChart::new("waterfall")
.step("a", 2.0)
.step("b", -1.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
BoxPlot::new("box")
.group("a", [1.0, 2.0, 3.0])
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
ViolinPlot::new("violin")
.group("a", [1.0, 2.0, 3.0])
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
CalendarHeatmap::new("days", date(2026, 1, 5), [1.0; 14]).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
SankeyChart::new("sankey", ["a", "b"])
.link(0, 1, 5.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
FunnelChart::new("funnel")
.stage("a", 2.0)
.stage("b", 1.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
NetworkGraph::new("network")
.node("a", 0)
.node("b", 0)
.edge(0, 1)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
ParallelCoordinates::new("parallel", ["a", "b"])
.record("r", [1.0, 2.0])
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
PieChart::new("pie")
.slice("a", 1.0)
.slice("b", 2.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
RadarChart::new("radar", ["a", "b", "c"])
.series(Series::new("s", [1.0, 2.0, 3.0]))
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
RealtimeChart::new("live", [1.0, 2.0], 2).into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
ChordDiagram::new("chord", ["a", "b"], pairs.to_vec()).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
GanttChart::new("gantt")
.task(Task::new("t", date(2026, 3, 1), date(2026, 3, 9)))
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
Sunburst::new("sun", [Slice::new("a", 1.0)]).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
BulletChart::new("bullets")
.bullet(Bullet::new("b", 5.0, 8.0))
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
Treemap::new("treemap")
.tile("a", 1.0)
.tile("b", 2.0)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
HeatmapChart::new("heatmap", ["a"])
.row("a", [1.0])
.into_any_element()
}),
];
assert_eq!(
widths,
[px(240.0); 22],
"each chart spans the card inside its padding"
);
}
+8 -2
View File
@@ -208,7 +208,10 @@ impl RenderOnce for Treemap {
rects.clone(),
rects,
);
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
tracked(
root,
@@ -406,7 +409,10 @@ impl RenderOnce for HeatmapChart {
sizes.hairline,
);
let legend = key(steps(ink), ("Less", "More"), cx);
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
let chart = tracked(
div().relative().flex_1().min_h_0(),
+12 -13
View File
@@ -410,6 +410,17 @@ impl RenderOnce for IndexManager {
);
let ready = !name.read(cx).text().trim().is_empty() && !picked.is_empty();
let [chose, flipped, pick_method, made] = [(); 4].map(|_| draft.clone());
let methods = Select::new(
(id.clone(), "method"),
METHODS.iter().map(|method| Choice::new(*method, *method)),
)
.selected(method)
.on_change(move |value, _, cx| {
pick_method.update(cx, |draft, cx| {
draft.method = value.clone();
cx.notify();
})
});
div()
.flex()
.flex_col()
@@ -453,19 +464,7 @@ impl RenderOnce for IndexManager {
})
}),
)
.child(
Select::new(
(id.clone(), "method"),
METHODS.iter().map(|method| Choice::new(*method, *method)),
)
.selected(method)
.on_change(move |value, _, cx| {
pick_method.update(cx, |draft, cx| {
draft.method = value.clone();
cx.notify();
})
}),
),
.child(div().flex_1().min_w_0().child(methods)),
)
.child(
Button::new((id, "create"), "Create index")
+1 -1
View File
@@ -248,7 +248,7 @@ impl RenderOnce for NotificationCenter {
div()
.flex_1()
.flex()
.items_center()
.flex_col()
.justify_center()
.child(
EmptyState::new(
+1 -1
View File
@@ -53,7 +53,7 @@ pub(super) fn state_view(
let theme = cx.theme();
let colors = &theme.colors;
div()
.w_full()
.debug_selector(|| "state-root".into())
.flex()
.flex_col()
.items_center()
+24 -1
View File
@@ -10,8 +10,10 @@ use super::{press, settle, setup, wait};
use crate::{
buttons::Button,
feedback::{
ConfirmationCard, Countdown, EmptyState, ErrorBoundary, SaveState, SavingIndicator,
ConfirmationCard, Countdown, EmptyState, ErrorBoundary, ErrorView, ResultView, SaveState,
SavingIndicator,
},
layout::tests::narrow_width,
primitives::{FocusScope, IconName, Measure},
};
@@ -200,3 +202,24 @@ fn a_long_inline_message_wraps_inside_its_box(cx: &mut TestAppContext) {
.expect("the words draw");
assert!(text.right() <= px(200.0), "{text:?}");
}
#[gpui::test]
fn state_views_fill_a_column_their_block_measures_by_content(cx: &mut TestAppContext) {
setup(cx);
let widths = [
narrow_width(cx, "state-root", |_, _| {
EmptyState::new("empty", IconName::Search, "Nothing here").into_any_element()
}),
narrow_width(cx, "state-root", |_, _| {
ErrorView::not_found("missing").into_any_element()
}),
narrow_width(cx, "state-root", |_, _| {
ResultView::success("done", "Saved").into_any_element()
}),
];
assert_eq!(
widths,
[px(240.0); 3],
"each view spans the card inside its padding"
);
}
+6 -3
View File
@@ -1,6 +1,6 @@
use gpui::{
App, Div, ElementId, Entity, IntoElement, ParentElement, PathBuilder, Pixels, Point,
Refineable, RenderOnce, StyleRefinement, Styled, Window, canvas, div, fill,
App, Div, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, PathBuilder,
Pixels, Point, Refineable, RenderOnce, StyleRefinement, Styled, Window, canvas, div, fill,
};
use super::quotes::moves;
@@ -250,7 +250,10 @@ impl RenderOnce for DepthChart {
let (stroke, bottom) = (sizes.stroke.to_pixels(rem), frame.y + frame.h);
let mark = reading.map(|(price, _, total)| (xs.at(price), ys.at(total)));
let palette = colors.clone();
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
tracked(
root,
+4 -1
View File
@@ -165,7 +165,10 @@ impl RenderOnce for MarketHeatmap {
theme.radius(Radius::Sm).to_pixels(rem),
colors.fg,
);
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(self.base.style());
tracked(
root,
+4 -1
View File
@@ -422,7 +422,10 @@ impl RenderOnce for CandlestickChart {
},
};
let height = sizes.height + sizes.height * 0.3 * self.studies.len() as f32;
let mut root = div().relative().w_full().h(height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(height);
root.style().refine(self.base.style());
let steering = Steering {
stage: stage.clone(),
+3 -1
View File
@@ -196,7 +196,9 @@ impl RenderOnce for PayoffDiagram {
}
chart.labels = labels;
chart.format = Rc::new(|value| format::currency(value, "USD"));
let mut base = div().w_full().h(cx.theme().chart().height);
let mut base = div()
.debug_selector(|| "chart-root".into())
.h(cx.theme().chart().height);
base.style().refine(self.base.style());
plot(chart, base, window, cx)
}
+6 -3
View File
@@ -1,8 +1,8 @@
use std::rc::Rc;
use gpui::{
App, Div, ElementId, Entity, IntoElement, ParentElement, PathBuilder, Pixels, Refineable,
RenderOnce, StyleRefinement, Styled, Window, canvas, div, fill,
App, Div, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, PathBuilder,
Pixels, Refineable, RenderOnce, StyleRefinement, Styled, Window, canvas, div, fill,
};
use jiff::tz::TimeZone;
@@ -146,7 +146,10 @@ fn board(
theme.radius(crate::theme::Radius::Sm).to_pixels(rem),
);
let box_px = boxes.map(|size| (ys.at(0.0) - ys.at(size)).abs());
let mut root = div().relative().w_full().h(sizes.height);
let mut root = div()
.debug_selector(|| "chart-root".into())
.relative()
.h(sizes.height);
root.style().refine(base.style());
tracked(root, (id, "marks").into(), &tiles, move |place| {
hit.iter().position(|(rect, _)| rect.contains(place))
+43 -2
View File
@@ -6,8 +6,11 @@ use gpui::{
};
use jiff::Timestamp;
use crate::finance::{CandlestickChart, ChartSync, candles::Candle, stage::Visible};
use crate::theme::Theme;
use crate::finance::{
CandlestickChart, ChartSync, DepthChart, Leg, MarketHeatmap, PayoffDiagram, PointFigureChart,
RenkoChart, candles::Candle, stage::Visible,
};
use crate::{layout::tests::narrow_width, theme::Theme};
fn candles(count: usize) -> Rc<Vec<Candle>> {
Rc::new(
@@ -106,3 +109,41 @@ fn a_lagging_chart_leaves_the_shared_window_alone(cx: &mut TestAppContext) {
"the lagging chart's append is already shown"
);
}
#[gpui::test]
fn every_chart_fills_a_column_its_block_measures_by_content(cx: &mut TestAppContext) {
cx.update(Theme::init);
let leg = Leg {
call: true,
strike: 100.0,
quantity: 1.0,
premium: 2.0,
};
let widths = [
narrow_width(cx, "chart-root", |_, _| {
DepthChart::new("depth", [(99.0, 2.0)], [(101.0, 3.0)]).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
MarketHeatmap::new("heatmap")
.tile("A", 1.0, 0.5)
.into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
CandlestickChart::new("candles", candles(30)).into_any_element()
}),
narrow_width(cx, "chart-root", move |_, _| {
PayoffDiagram::new("payoff", [leg], (80.0, 120.0), 100.0).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
RenkoChart::new("renko", candles(30), 0.1).into_any_element()
}),
narrow_width(cx, "chart-root", |_, _| {
PointFigureChart::new("figure", candles(30), 0.1, 3).into_any_element()
}),
];
assert_eq!(
widths,
[px(240.0); 6],
"each chart spans the card inside its padding"
);
}
+1 -1
View File
@@ -43,10 +43,10 @@ fn checked(
let theme = cx.theme();
let (subtle, danger) = (theme.colors.fg_subtle, theme.colors.danger);
div()
.debug_selector(|| "checked-root".into())
.flex()
.flex_col()
.gap_1()
.w_full()
.child(
Input::new(state)
.size(size)
+1 -1
View File
@@ -172,9 +172,9 @@ impl Suggestions {
let (up_rows, down_rows, enter_rows) = (rows.clone(), rows.clone(), rows.clone());
let (enter, trigger) = (self.pick.clone(), self.trigger);
div()
.debug_selector(|| "mention-root".into())
.id(self.id)
.relative()
.w_full()
.capture_action(move |_: &Up, _, cx| {
if open {
cx.stop_propagation();
+1 -1
View File
@@ -401,10 +401,10 @@ impl RenderOnce for NumberInput {
.child(stepper),
);
div()
.debug_selector(|| "number-root".into())
.flex()
.items_center()
.gap_2()
.w_full()
.children(label)
.child(
div()
+1 -1
View File
@@ -88,13 +88,13 @@ pub(crate) fn field_button(
let theme = cx.theme();
let colors = &theme.colors;
div()
.debug_selector(|| "field-root".into())
.id(id)
.track_focus(focus)
.relative()
.flex()
.items_center()
.gap_2()
.w_full()
.h(theme.control_height(size))
.px(theme.control_padding(size))
.rounded(theme.radius(Radius::Md))
+2 -1
View File
@@ -243,13 +243,14 @@ impl Track {
})
});
div()
.debug_selector(|| "slider-root".into())
.id(self.id)
.flex_none()
.map(|outer| {
if vertical {
outer.h_full().w(thumb).py(thumb * 0.5)
} else {
outer.w_full().h(thumb).px(thumb * 0.5)
outer.h(thumb).px(thumb * 0.5)
}
})
.when(self.disabled, |outer| outer.opacity(0.5))
+15 -11
View File
@@ -67,14 +67,16 @@ struct Picker {
impl Render for Picker {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
Select::new("select", abc(false))
.selected(self.chosen.clone())
.on_change(move |value, _, cx| {
view.update(cx, |view, cx| {
view.chosen = value.clone();
cx.notify();
})
})
div().size_full().child(
Select::new("select", abc(false))
.selected(self.chosen.clone())
.on_change(move |value, _, cx| {
view.update(cx, |view, cx| {
view.chosen = value.clone();
cx.notify();
})
}),
)
}
}
@@ -425,9 +427,11 @@ struct Empty(bool);
impl Render for Empty {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Select::new("empty", [])
.placeholder("Nothing yet")
.disabled(self.0)
div().size_full().child(
Select::new("empty", [])
.placeholder("Nothing yet")
.disabled(self.0),
)
}
}
+37 -26
View File
@@ -1,4 +1,7 @@
use gpui::{Context, IntoElement, KeyUpEvent, Keystroke, Render, TestAppContext, Window};
use gpui::{
Context, IntoElement, KeyUpEvent, Keystroke, ParentElement, Render, Styled, TestAppContext,
Window, div,
};
use jiff::civil::{Date, Time, date, time};
use super::setup;
@@ -14,15 +17,17 @@ struct Day {
impl Render for Day {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
DatePicker::new("day", self.value)
.today(TODAY)
.disabled(self.disabled)
.on_change(move |picked, _, cx| {
view.update(cx, |view, cx| {
view.value = Some(picked);
cx.notify();
})
})
div().size_full().child(
DatePicker::new("day", self.value)
.today(TODAY)
.disabled(self.disabled)
.on_change(move |picked, _, cx| {
view.update(cx, |view, cx| {
view.value = Some(picked);
cx.notify();
})
}),
)
}
}
@@ -48,14 +53,16 @@ struct Span {
impl Render for Span {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
DateRangePicker::new("span", self.value)
.today(TODAY)
.on_change(move |picked, _, cx| {
view.update(cx, |view, cx| {
view.value = Some(picked);
cx.notify();
})
})
div().size_full().child(
DateRangePicker::new("span", self.value)
.today(TODAY)
.on_change(move |picked, _, cx| {
view.update(cx, |view, cx| {
view.value = Some(picked);
cx.notify();
})
}),
)
}
}
@@ -78,14 +85,18 @@ struct Clock {
impl Render for Clock {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let view = cx.entity();
TimePicker::new("clock", self.value)
.step(15)
.on_change(move |picked, _, cx| {
view.update(cx, |view, cx| {
view.value = Some(picked);
cx.notify();
})
})
div()
.size_full()
.child(
TimePicker::new("clock", self.value)
.step(15)
.on_change(move |picked, _, cx| {
view.update(cx, |view, cx| {
view.value = Some(picked);
cx.notify();
})
}),
)
}
}
+38 -2
View File
@@ -1,12 +1,16 @@
use gpui::{
AppContext as _, Context, Entity, InteractiveElement, IntoElement, KeyUpEvent, Keystroke,
App, AppContext as _, Context, Entity, InteractiveElement, IntoElement, KeyUpEvent, Keystroke,
ParentElement, Render, Styled, TestAppContext, VisualTestContext, Window, div, px,
};
use super::setup;
use crate::{
buttons::Button,
forms::{FieldArray, Form, FormField, Input, InputEvent, TextInput},
forms::{
Choice, DatePicker, EmailInput, FieldArray, Form, FormField, Input, InputEvent,
MentionInput, NumberInput, Select, Slider, TextInput,
},
layout::tests::narrow_width,
};
struct Signup {
@@ -195,3 +199,35 @@ fn a_field_keeps_inside_its_cards_padding(cx: &mut TestAppContext) {
"a percent min would take the card's outer width"
);
}
#[gpui::test]
fn fields_fill_a_column_their_block_measures_by_content(cx: &mut TestAppContext) {
setup(cx);
let field =
|window: &mut Window, cx: &mut App| window.use_keyed_state("field", cx, TextInput::new);
let widths = [
narrow_width(cx, "field-root", |_, _| {
Select::new("select", [Choice::new("a", "A")]).into_any_element()
}),
narrow_width(cx, "field-root", |_, _| {
DatePicker::new("date", None).into_any_element()
}),
narrow_width(cx, "checked-root", move |window, cx| {
EmailInput::new(&field(window, cx)).into_any_element()
}),
narrow_width(cx, "mention-root", move |window, cx| {
MentionInput::new("mention", &field(window, cx)).into_any_element()
}),
narrow_width(cx, "slider-root", |_, _| {
Slider::new("slider", 0.5).into_any_element()
}),
narrow_width(cx, "number-root", |_, _| {
NumberInput::new("number", 42.0).into_any_element()
}),
];
assert_eq!(
widths,
[px(240.0); 6],
"each field spans the card inside its padding"
);
}
+2
View File
@@ -12,6 +12,8 @@ mod sidebar;
mod split;
mod stack;
mod surface;
#[cfg(all(test, feature = "test-support"))]
pub(crate) mod tests;
mod viewport;
mod workspace;
+34
View File
@@ -0,0 +1,34 @@
use gpui::{
AnyElement, App, Context, IntoElement, ParentElement, Pixels, Render, Styled, TestAppContext,
Window, div, px,
};
type Build = Box<dyn Fn(&mut Window, &mut App) -> AnyElement>;
/// One element in a column that a padded block measures by content.
struct Narrow(Build);
impl Render for Narrow {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().w(px(280.0)).flex().flex_col().child(
div().p_5().child(
div()
.flex()
.flex_col()
.child((self.0)(window, cx))
.child("A line long enough to wrap in a narrow card, so the column fills it."),
),
)
}
}
/// The width of `selector` in that column; a root that fills it takes all 240px.
pub(crate) fn narrow_width(
cx: &mut TestAppContext,
selector: &'static str,
build: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
) -> Pixels {
let (_, cx) = cx.add_window_view(|_, _| Narrow(Box::new(build)));
cx.run_until_parked();
cx.debug_bounds(selector).expect(selector).size.width
}
+1 -1
View File
@@ -200,10 +200,10 @@ impl RenderOnce for Steps {
}
});
div()
.debug_selector(|| "steps-root".into())
.id(self.id)
.flex()
.when(vertical, |steps| steps.flex_col())
.w_full()
.children(steps)
}
}
+15 -1
View File
@@ -6,10 +6,11 @@ use gpui::{
use super::{
BackForwardNavigation, Breadcrumb, Crumb, EditorTab, EditorTabs, GoToLine, LoadMore,
NavigationMenu, Tabs, Wizard,
NavigationMenu, Steps, Tabs, Wizard,
};
use crate::{
forms::{Choice, TextInput},
layout::tests::narrow_width,
theme::Theme,
};
@@ -456,3 +457,16 @@ fn load_more_asks_once_per_press_and_goes_once_all_show(cx: &mut TestAppContext)
"all shown, no button"
);
}
#[gpui::test]
fn steps_fill_a_column_their_block_measures_by_content(cx: &mut TestAppContext) {
setup(cx);
let width = narrow_width(cx, "steps-root", |_, _| {
Steps::new("steps", [Choice::new("a", "A"), Choice::new("b", "B")], 0).into_any_element()
});
assert_eq!(
width,
px(240.0),
"the steps span the card inside its padding"
);
}