From 5f9102751e77f01e93404fbf2925b01de8f078d1 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Wed, 30 Sep 2026 21:05:17 -0700 Subject: [PATCH] button: carry the caller's alignment to the content row A Button draws its content in an h_flex that fills the root (size_full) and centers its children (items_center, justify_center). Styles set on the Button, justify_start() or items_start(), refined only the root, where they have nothing left to place because the content row already fills it. The row itself was refined only by the crate-internal content_style. So a row-shaped button, ghost().w_full().justify_start(), still centered its label and children, and a list of such rows rendered staggered, each centered at its own width. The content row now repeats the caller's justify_content and align_items after content_style. A Button asked for nothing keeps its centered content, and a dropdown caret still spreads the row with justify_between. Found in the Codewhale native golden-path walk (app source 3ff67bb, kit pin 60eeba2c): the My work "Connected work" rows rendered centered and staggered under a left-aligned header. - Test: button::button::tests:: full_width_button_places_content_by_its_alignment. A 400px justify_start button puts its child at the leading padding; the same button asked for nothing keeps the child centered (equal margins); an items_start button 60px tall puts its child at the top edge. - Docs: Button "Content Alignment" example, en and zh-CN. - Other kit callers: two story rows (collapsible, combobox) asked for justify_start and now get it. The base showcase buttons are gpui_base::Button and do not pass through this code. Evidence (local, macOS arm64, cargowhale governor), on e3dfda2f plus this change: - cargo test -p gpui-component --lib: 530 passed, 0 failed, 0 ignored - cargo clippy -p gpui-component --lib --tests -- --deny warnings: clean - cargo fmt --all -- --check: clean - script/check-ai docs: PASS (9 published recipe fragments) Not run locally: typos (not installed), other crates' tests, other platforms. The new test was not run against the pre-fix Button; the Codewhale app's own guard fails on the old pin and is recorded there. Co-Authored-By: Claude Opus 5.5 --- crates/component/src/button/button.rs | 77 +++++++++++++++++++++++++++ website/component/button.md | 22 ++++++++ website/zh-CN/component/button.md | 20 +++++++ 3 files changed, 119 insertions(+) diff --git a/crates/component/src/button/button.rs b/crates/component/src/button/button.rs index 40785496ea..c924635280 100644 --- a/crates/component/src/button/button.rs +++ b/crates/component/src/button/button.rs @@ -574,6 +574,13 @@ impl RenderOnce for Button { let mut base = self.base; let children = self.children; let instance_style = base.style().clone(); + // The content row fills the root, so alignment set on the button only + // reaches what it shows when the row repeats it. + let content_alignment = StyleRefinement { + justify_content: instance_style.justify_content, + align_items: instance_style.align_items, + ..Default::default() + }; let normal_style = style.normal(self.outline, cx); let selected_style = style.selected(self.outline, cx); let disabled_style = style.disabled(self.outline, cx); @@ -710,6 +717,7 @@ impl RenderOnce for Button { _ => this.gap_2(), }) .refine_style(&self.content_style) + .refine_style(&content_alignment) .when_some(self.icon, |this, icon| { this.child( icon.loading_icon(self.loading_icon) @@ -1731,6 +1739,75 @@ mod tests { assert!(bounds.size.height > px(0.)); } + /// A row-shaped button — full width, content on the left — is built by + /// asking the button for `justify_start`. The content row fills the root, + /// so the request has to reach that row, or every such row is centered. + #[gpui::test] + fn full_width_button_places_content_by_its_alignment(cx: &mut gpui::TestAppContext) { + use gpui::{Context, Render}; + + struct AlignmentHarness; + + fn row(id: &'static str) -> Button { + Button::new(id).ghost().w_full().child( + div() + .debug_selector(move || format!("{id}-content")) + .child("Row"), + ) + } + + impl Render for AlignmentHarness { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div() + .w(px(400.)) + .child( + row("start") + .justify_start() + .debug_selector(|| "start".into()), + ) + .child(row("centered").debug_selector(|| "centered".into())) + .child( + row("top") + .h(px(60.)) + .items_start() + .debug_selector(|| "top".into()), + ) + } + } + + cx.update(crate::init); + let (_, cx) = cx.add_window_view(|_, _| AlignmentHarness); + cx.update(|window, cx| window.draw(cx).clear(cx)); + let mut bounds = |selector: &'static str| { + cx.debug_bounds(selector) + .unwrap_or_else(|| panic!("{selector} must be laid out")) + }; + + let (start, start_content) = (bounds("start"), bounds("start-content")); + assert_eq!(start.size.width, px(400.)); + assert!( + start_content.left() - start.left() < px(16.), + "justify_start must put the content at the button's leading padding, \ + got {start_content:?} in {start:?}" + ); + + let (centered, centered_content) = (bounds("centered"), bounds("centered-content")); + let leading = centered_content.left() - centered.left(); + let trailing = centered.right() - centered_content.right(); + assert!( + leading > px(100.) && (leading - trailing).abs() < px(1.), + "a button asked for nothing keeps its content centered, \ + got {centered_content:?} in {centered:?}" + ); + + let (top, top_content) = (bounds("top"), bounds("top-content")); + assert_eq!( + top_content.top(), + top.top(), + "items_start must put the content at the button's top edge" + ); + } + #[gpui::test] fn test_outline_selected_uses_outline_active_style(cx: &mut gpui::TestAppContext) { cx.update(crate::init); diff --git a/website/component/button.md b/website/component/button.md index bf6fded231..2c70ee5a59 100644 --- a/website/component/button.md +++ b/website/component/button.md @@ -363,6 +363,28 @@ Button::new("btn") ) ``` +### Content Alignment + +A button centers its content. Alignment set on the button places the content +instead, so a full-width row keeps its label and children on the leading edge: + +```rust +Button::new("row") + .ghost() + .w_full() + .h_auto() + .justify_start() + .child( + v_flex() + .items_start() + .child("Pull requests") + .child("GitHub"), + ) +``` + +`items_start()` and `items_end()` likewise place the content vertically in a +button taller than it. + [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html [ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html diff --git a/website/zh-CN/component/button.md b/website/zh-CN/component/button.md index a753b967ce..c0c4ef8c4b 100644 --- a/website/zh-CN/component/button.md +++ b/website/zh-CN/component/button.md @@ -323,6 +323,26 @@ Button::new("btn") ) ``` +### 内容对齐 + +按钮默认将内容居中。在按钮上设置的对齐方式会作用于内容本身,因此整行宽度的按钮也能让标签和子内容靠起始边排列: + +```rust +Button::new("row") + .ghost() + .w_full() + .h_auto() + .justify_start() + .child( + v_flex() + .items_start() + .child("Pull requests") + .child("GitHub"), + ) +``` + +同样,当按钮比内容高时,`items_start()` 和 `items_end()` 决定内容在垂直方向上的位置。 + [Button]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.Button.html [ButtonGroup]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonGroup.html [ButtonCustomVariant]: https://docs.rs/gpui-component/latest/gpui_component/button/struct.ButtonCustomVariant.html