From ff1e97132ef01db7857281a5097fa8b5607a84e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 27 Sep 2026 18:04:05 -0400 Subject: [PATCH] =?UTF-8?q?Theme=20=C2=B7=20Platform:=20caption=20buttons?= =?UTF-8?q?=20of=20their=20own=20height,=20titles=20that=20give=20way?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows caption buttons take the bar's height less its hairline, a new token, instead of their row's, so they keep their height beside caps that wrap below them. A title bar's title is an Ellipsis: on Windows it takes the free width before the buttons and gives way; a centered title sits inset past the lights on both sides. The controls never shrink. Tests hold a long Windows title's close button inside a 280px bar and wrapped caps inside their row. --- TASKS.md | 1 + examples/gallery/pages/shell/chrome.rs | 2 +- src/shell/chrome.rs | 20 ++++++--- src/shell/tests.rs | 60 ++++++++++++++++++++++++++ src/theme/tokens.rs | 5 +++ 5 files changed, 80 insertions(+), 8 deletions(-) diff --git a/TASKS.md b/TASKS.md index be8ff31..675250c 100644 --- a/TASKS.md +++ b/TASKS.md @@ -424,3 +424,4 @@ Per-component lines live in `tasks/`. Tags there: | T39a | 3 | PASS | | | T39b | 1 | FAIL | a long theme name pushed its badge past 280px; four-digit colors refused the theme | | T39b | 2 | PASS | | +| T39c | 1 | FAIL | Windows caption buttons took their row's height; a long Windows title pushed the buttons past the bar | diff --git a/examples/gallery/pages/shell/chrome.rs b/examples/gallery/pages/shell/chrome.rs index 0a65fa0..b9749fb 100644 --- a/examples/gallery/pages/shell/chrome.rs +++ b/examples/gallery/pages/shell/chrome.rs @@ -59,7 +59,7 @@ pub fn title_bar(cx: &App) -> impl IntoElement + use<> { })), ) .child(code( - "TitleBar::new(id) with no style leaves room for the real macOS lights.", + "TitleBar::new(id) with no platform leaves room for the real macOS lights.", cx, )) } diff --git a/src/shell/chrome.rs b/src/shell/chrome.rs index 9bfcd78..624e6ee 100644 --- a/src/shell/chrome.rs +++ b/src/shell/chrome.rs @@ -10,6 +10,7 @@ use smallvec::SmallVec; use crate::{ primitives::{Icon, IconName}, theme::{ActiveTheme, ControlSize, IconSize, Platform, TextSize}, + typography::Ellipsis, }; /// Drags the window, and zooms it on double-click. @@ -174,7 +175,7 @@ fn caption_buttons( div() .id(id) .flex() - .h_full() + .flex_none() .children(buttons.map(|(control, icon, area)| { let on_close = on_close.clone(); let group = SharedString::from(format!("caption-{control:?}")); @@ -189,7 +190,8 @@ fn caption_buttons( .items_center() .justify_center() .w(theme.caption_button_width()) - .h_full() + .h(theme.caption_button_height()) + .debug_selector(move || format!("caption-{control:?}")) .window_control_area(area) .hover(|style| style.bg(hover)) .on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) @@ -357,10 +359,11 @@ impl RenderOnce for TitleBar { }; let title = self.title.map(|title| { div() + .min_w_0() .text_size(theme.text_size(TextSize::Sm)) .font_weight(FontWeight::MEDIUM) .text_color(theme.colors.fg_muted) - .child(title) + .child(Ellipsis::new(title)) }); let centered = style != Platform::Windows; let inset = theme.traffic_light_inset(); @@ -385,12 +388,15 @@ impl RenderOnce for TitleBar { }; let bar = bar.children(self.leading); let bar = match (centered, title) { - (false, Some(title)) => bar.child(title), + (false, Some(title)) => bar.child(title.flex_1()), (_, title) => bar.when_some(title, |bar, title| { bar.child( div() .absolute() - .inset_0() + .top_0() + .bottom_0() + .left(inset) + .right(inset) .flex() .items_center() .justify_center() @@ -398,8 +404,8 @@ impl RenderOnce for TitleBar { ) }), }; - bar.child(div().flex_1()) + bar.when(centered, |bar| bar.child(div().flex_1())) .children(self.actions) - .children(trailing) + .children(trailing.map(|controls| div().flex_none().child(controls))) } } diff --git a/src/shell/tests.rs b/src/shell/tests.rs index efe02c5..5bb1338 100644 --- a/src/shell/tests.rs +++ b/src/shell/tests.rs @@ -130,3 +130,63 @@ fn the_themes_platform_draws_the_buttons_and_the_caps(cx: &mut TestAppContext) { "Ctrl wider than the command mark" ); } + +struct LongTitle; + +impl Render for LongTitle { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + div().w(px(280.0)).flex().flex_col().child( + super::TitleBar::new("bar") + .title("Ely — Projects at a narrow width, and more besides") + .platform(crate::theme::Platform::Windows), + ) + } +} + +/// At 280px a Windows bar's long title gives way, and its close button ends inside the bar. +#[gpui::test] +fn a_long_title_leaves_the_caption_buttons_in_the_bar(cx: &mut TestAppContext) { + cx.update(Theme::init); + let (_, cx) = cx.add_window_view(|_, _| LongTitle); + settle(cx); + let close = cx.debug_bounds("caption-Close").expect("a close button"); + assert!(close.right() <= px(280.0), "{close:?} inside the bar"); +} + +struct Wrapped; + +impl Render for Wrapped { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let windows = crate::theme::Platform::Windows; + div() + .w(px(280.0)) + .flex() + .flex_col() + .child( + div() + .debug_selector(|| "row".into()) + .flex() + .flex_wrap() + .items_center() + .gap_3() + .child(super::WindowControls::new("controls").platform(windows)) + .child(div().debug_selector(|| "combo".into()).child( + crate::typography::KbdCombo::new("secondary-shift-k secondary-shift-s"), + )), + ) + .child(div().h(px(20.0))) + } +} + +/// Caption buttons keep their own height beside caps that wrap below them, so the caps end inside the row. +#[gpui::test] +fn caption_buttons_keep_their_height_in_a_wrapping_row(cx: &mut TestAppContext) { + cx.update(Theme::init); + cx.update(|cx| Theme::update(cx, |theme| theme.platform = crate::theme::Platform::Windows)); + let (_, cx) = cx.add_window_view(|_, _| Wrapped); + settle(cx); + let row = cx.debug_bounds("row").expect("the row"); + let combo = cx.debug_bounds("combo").expect("the caps"); + assert!(combo.top() > row.top(), "the caps wrap below the buttons"); + assert!(combo.bottom() <= row.bottom(), "{combo:?} inside {row:?}"); +} diff --git a/src/theme/tokens.rs b/src/theme/tokens.rs index 5c45bdb..fdbab9d 100644 --- a/src/theme/tokens.rs +++ b/src/theme/tokens.rs @@ -230,6 +230,11 @@ impl Theme { px_to_rems(46.0) } + /// A Windows caption button's height: the title bar's, less its bottom hairline. + pub fn caption_button_height(&self) -> Pixels { + self.titlebar_height().to_pixels(self.base_rem()) - px(1.0) + } + /// Resize margin of a client-drawn window. pub fn window_inset(&self) -> Rems { px_to_rems(10.0)