diff --git a/AGENTS.md b/AGENTS.md index 1855b6f..b6c82b7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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>`. 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()`. diff --git a/TASKS.md b/TASKS.md index 9ec4b8f..4957d2a 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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 | diff --git a/examples/gallery/pages/feedback/messages.rs b/examples/gallery/pages/feedback/messages.rs index 31bf43a..fdc4bdb 100644 --- a/examples/gallery/pages/feedback/messages.rs +++ b/examples/gallery/pages/feedback/messages.rs @@ -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() diff --git a/examples/gallery/pages/feedback/states.rs b/examples/gallery/pages/feedback/states.rs index c494e58..cc2c4fb 100644 --- a/examples/gallery/pages/feedback/states.rs +++ b/examples/gallery/pages/feedback/states.rs @@ -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() diff --git a/src/agent/previews.rs b/src/agent/previews.rs index 0e2c713..c100de3 100644 --- a/src/agent/previews.rs +++ b/src/agent/previews.rs @@ -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() diff --git a/src/agent/tests/previews.rs b/src/agent/tests/previews.rs index aa22321..50b823d 100644 --- a/src/agent/tests/previews.rs +++ b/src/agent/tests/previews.rs @@ -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" + ); +} diff --git a/src/charts/calendar.rs b/src/charts/calendar.rs index 039549e..1b8dd16 100644 --- a/src/charts/calendar.rs +++ b/src/charts/calendar.rs @@ -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::>(); 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(), diff --git a/src/charts/cartesian.rs b/src/charts/cartesian.rs index c536f39..ae961e0 100644 --- a/src/charts/cartesian.rs +++ b/src/charts/cartesian.rs @@ -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) } diff --git a/src/charts/flow.rs b/src/charts/flow.rs index 8793b93..818e14c 100644 --- a/src/charts/flow.rs +++ b/src/charts/flow.rs @@ -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| { diff --git a/src/charts/funnel.rs b/src/charts/funnel.rs index 4b7afb1..f8ac018 100644 --- a/src/charts/funnel.rs +++ b/src/charts/funnel.rs @@ -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| { diff --git a/src/charts/network.rs b/src/charts/network.rs index bb426ab..c51e6cf 100644 --- a/src/charts/network.rs +++ b/src/charts/network.rs @@ -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| { diff --git a/src/charts/parallel.rs b/src/charts/parallel.rs index 5d4dacc..de50bd5 100644 --- a/src/charts/parallel.rs +++ b/src/charts/parallel.rs @@ -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, diff --git a/src/charts/pie.rs b/src/charts/pie.rs index de11c3b..6d16c7b 100644 --- a/src/charts/pie.rs +++ b/src/charts/pie.rs @@ -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() diff --git a/src/charts/radar.rs b/src/charts/radar.rs index c7b79ea..1be6c12 100644 --- a/src/charts/radar.rs +++ b/src/charts/radar.rs @@ -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() diff --git a/src/charts/realtime.rs b/src/charts/realtime.rs index a97d441..c1211fa 100644 --- a/src/charts/realtime.rs +++ b/src/charts/realtime.rs @@ -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( diff --git a/src/charts/relations.rs b/src/charts/relations.rs index ee8cc22..01eb0b9 100644 --- a/src/charts/relations.rs +++ b/src/charts/relations.rs @@ -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, diff --git a/src/charts/schedule.rs b/src/charts/schedule.rs index 266c5eb..5e6f8d0 100644 --- a/src/charts/schedule.rs +++ b/src/charts/schedule.rs @@ -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( diff --git a/src/charts/sunburst.rs b/src/charts/sunburst.rs index 3f8189d..0e38f27 100644 --- a/src/charts/sunburst.rs +++ b/src/charts/sunburst.rs @@ -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")) diff --git a/src/charts/targets.rs b/src/charts/targets.rs index aec26c2..7b94062 100644 --- a/src/charts/targets.rs +++ b/src/charts/targets.rs @@ -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( diff --git a/src/charts/tests.rs b/src/charts/tests.rs index 9ad9a72..6bd47ed 100644 --- a/src/charts/tests.rs +++ b/src/charts/tests.rs @@ -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" + ); +} diff --git a/src/charts/tiles.rs b/src/charts/tiles.rs index 4b5e775..16caf1a 100644 --- a/src/charts/tiles.rs +++ b/src/charts/tiles.rs @@ -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(), diff --git a/src/devtools/structure.rs b/src/devtools/structure.rs index e1697b1..7e14f68 100644 --- a/src/devtools/structure.rs +++ b/src/devtools/structure.rs @@ -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") diff --git a/src/feedback/notification.rs b/src/feedback/notification.rs index d96b72c..bf558aa 100644 --- a/src/feedback/notification.rs +++ b/src/feedback/notification.rs @@ -248,7 +248,7 @@ impl RenderOnce for NotificationCenter { div() .flex_1() .flex() - .items_center() + .flex_col() .justify_center() .child( EmptyState::new( diff --git a/src/feedback/states.rs b/src/feedback/states.rs index 7ae69fc..208bc0f 100644 --- a/src/feedback/states.rs +++ b/src/feedback/states.rs @@ -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() diff --git a/src/feedback/tests/states.rs b/src/feedback/tests/states.rs index 7f4a77e..08e720c 100644 --- a/src/feedback/tests/states.rs +++ b/src/feedback/tests/states.rs @@ -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" + ); +} diff --git a/src/finance/depth.rs b/src/finance/depth.rs index 0f72451..592d6b9 100644 --- a/src/finance/depth.rs +++ b/src/finance/depth.rs @@ -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, diff --git a/src/finance/heatmap.rs b/src/finance/heatmap.rs index d2514fc..205b6c4 100644 --- a/src/finance/heatmap.rs +++ b/src/finance/heatmap.rs @@ -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, diff --git a/src/finance/market.rs b/src/finance/market.rs index e7d1bb9..455ac85 100644 --- a/src/finance/market.rs +++ b/src/finance/market.rs @@ -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(), diff --git a/src/finance/payoff.rs b/src/finance/payoff.rs index af37260..9ff91dc 100644 --- a/src/finance/payoff.rs +++ b/src/finance/payoff.rs @@ -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) } diff --git a/src/finance/renko.rs b/src/finance/renko.rs index d46e736..e51457f 100644 --- a/src/finance/renko.rs +++ b/src/finance/renko.rs @@ -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)) diff --git a/src/finance/tests.rs b/src/finance/tests.rs index 1e405a0..5a659c6 100644 --- a/src/finance/tests.rs +++ b/src/finance/tests.rs @@ -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> { 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" + ); +} diff --git a/src/forms/checked.rs b/src/forms/checked.rs index 7854617..3842ea5 100644 --- a/src/forms/checked.rs +++ b/src/forms/checked.rs @@ -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) diff --git a/src/forms/mention.rs b/src/forms/mention.rs index e1d682d..efd5e2e 100644 --- a/src/forms/mention.rs +++ b/src/forms/mention.rs @@ -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(); diff --git a/src/forms/number.rs b/src/forms/number.rs index b67b8fd..6b9a67f 100644 --- a/src/forms/number.rs +++ b/src/forms/number.rs @@ -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() diff --git a/src/forms/select.rs b/src/forms/select.rs index 3884000..804eb7e 100644 --- a/src/forms/select.rs +++ b/src/forms/select.rs @@ -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)) diff --git a/src/forms/slider.rs b/src/forms/slider.rs index 1ee122f..7dc5770 100644 --- a/src/forms/slider.rs +++ b/src/forms/slider.rs @@ -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)) diff --git a/src/forms/tests/choices.rs b/src/forms/tests/choices.rs index 0e544ab..c93e506 100644 --- a/src/forms/tests/choices.rs +++ b/src/forms/tests/choices.rs @@ -67,14 +67,16 @@ struct Picker { impl Render for Picker { fn render(&mut self, _: &mut Window, cx: &mut Context) -> 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) -> impl IntoElement { - Select::new("empty", []) - .placeholder("Nothing yet") - .disabled(self.0) + div().size_full().child( + Select::new("empty", []) + .placeholder("Nothing yet") + .disabled(self.0), + ) } } diff --git a/src/forms/tests/dates.rs b/src/forms/tests/dates.rs index 09ee490..4cd01f6 100644 --- a/src/forms/tests/dates.rs +++ b/src/forms/tests/dates.rs @@ -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) -> 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) -> 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) -> 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(); + }) + }), + ) } } diff --git a/src/forms/tests/structure.rs b/src/forms/tests/structure.rs index 5e53d1d..18722ba 100644 --- a/src/forms/tests/structure.rs +++ b/src/forms/tests/structure.rs @@ -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" + ); +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 63da417..597c58c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -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; diff --git a/src/layout/tests.rs b/src/layout/tests.rs new file mode 100644 index 0000000..ad223f6 --- /dev/null +++ b/src/layout/tests.rs @@ -0,0 +1,34 @@ +use gpui::{ + AnyElement, App, Context, IntoElement, ParentElement, Pixels, Render, Styled, TestAppContext, + Window, div, px, +}; + +type Build = Box 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) -> 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 +} diff --git a/src/navigation/steps.rs b/src/navigation/steps.rs index 8236777..da5a6f1 100644 --- a/src/navigation/steps.rs +++ b/src/navigation/steps.rs @@ -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) } } diff --git a/src/navigation/tests.rs b/src/navigation/tests.rs index 05927ba..88a678b 100644 --- a/src/navigation/tests.rs +++ b/src/navigation/tests.rs @@ -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" + ); +}