diff --git a/TASKS.md b/TASKS.md index 675250c..3f59e34 100644 --- a/TASKS.md +++ b/TASKS.md @@ -425,3 +425,4 @@ Per-component lines live in `tasks/`. Tags there: | 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 | +| T39c | 2 | FAIL | an untitled Windows bar lost its spacer; a centered title ran under the Linux buttons; caption buttons ignored the window's rem | diff --git a/src/shell/chrome.rs b/src/shell/chrome.rs index 624e6ee..392f570 100644 --- a/src/shell/chrome.rs +++ b/src/shell/chrome.rs @@ -172,6 +172,7 @@ fn caption_buttons( (Control::Maximize, maximize, WindowControlArea::Max), (Control::Close, IconName::X, WindowControlArea::Close), ]; + let height = theme.caption_button_height(window.rem_size()); div() .id(id) .flex() @@ -190,7 +191,7 @@ fn caption_buttons( .items_center() .justify_center() .w(theme.caption_button_width()) - .h(theme.caption_button_height()) + .h(height) .debug_selector(move || format!("caption-{control:?}")) .window_control_area(area) .hover(|style| style.bg(hover)) @@ -363,6 +364,7 @@ impl RenderOnce for TitleBar { .text_size(theme.text_size(TextSize::Sm)) .font_weight(FontWeight::MEDIUM) .text_color(theme.colors.fg_muted) + .debug_selector(|| "titlebar-title".into()) .child(Ellipsis::new(title)) }); let centered = style != Platform::Windows; @@ -379,33 +381,44 @@ impl RenderOnce for TitleBar { .bg(theme.colors.bg) .border_b_1() .border_color(theme.colors.border); - let bar = match (system, style) { - (true, _) if fullscreen => bar.pl_3().pr_2(), - (true, _) => bar.pl(inset).pr_2(), - (false, Platform::Mac) => bar.pl_3().pr_2().children(leading), - (false, Platform::Windows) => bar.pl_3(), - (false, Platform::Linux) => bar.pl_3().pr_2(), - }; - let bar = bar.children(self.leading); - let bar = match (centered, title) { - (false, Some(title)) => bar.child(title.flex_1()), - (_, title) => bar.when_some(title, |bar, title| { - bar.child( + let bar = bar.when(!centered, |bar| bar.pl_3()); + let start = div().flex().child( + div() + .flex() + .items_center() + .gap_2() + .when(centered, |start| match system && !fullscreen { + true => start.pl(inset), + false => start.pl_3(), + }) + .children(leading) + .children(self.leading), + ); + let end = div().flex().justify_end().child( + div() + .flex() + .items_center() + .gap_2() + .when(centered, |end| end.pr_2()) + .children(self.actions) + .children(trailing.map(|controls| { div() - .absolute() - .top_0() - .bottom_0() - .left(inset) - .right(inset) - .flex() - .items_center() - .justify_center() - .child(title), - ) - }), + .flex_none() + .debug_selector(|| "titlebar-controls".into()) + .child(controls) + })), + ); + let middle = match title { + Some(title) if centered => title, + Some(title) => title.flex_1(), + None => div().flex_1(), }; - bar.when(centered, |bar| bar.child(div().flex_1())) - .children(self.actions) - .children(trailing.map(|controls| div().flex_none().child(controls))) + match centered { + true => bar.child(start.flex_1()).child(middle).child(end.flex_1()), + false => bar + .child(start.flex_none()) + .child(middle) + .child(end.flex_none()), + } } } diff --git a/src/shell/tests.rs b/src/shell/tests.rs index 5bb1338..75f0f51 100644 --- a/src/shell/tests.rs +++ b/src/shell/tests.rs @@ -190,3 +190,74 @@ fn caption_buttons_keep_their_height_in_a_wrapping_row(cx: &mut TestAppContext) assert!(combo.top() > row.top(), "the caps wrap below the buttons"); assert!(combo.bottom() <= row.bottom(), "{combo:?} inside {row:?}"); } + +struct Bar(Option<&'static str>, crate::theme::Platform); + +impl Render for Bar { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let bar = super::TitleBar::new("bar").platform(self.1); + div() + .w(px(280.0)) + .flex() + .flex_col() + .child( + div() + .debug_selector(|| "bar-box".into()) + .child(match self.0 { + Some(title) => bar.title(title), + None => bar, + }), + ) + } +} + +/// A Windows bar with no title still keeps its buttons at its right end. +#[gpui::test] +fn an_untitled_windows_bar_keeps_its_buttons_at_the_end(cx: &mut TestAppContext) { + cx.update(Theme::init); + let (_, cx) = cx.add_window_view(|_, _| Bar(None, crate::theme::Platform::Windows)); + settle(cx); + let close = cx.debug_bounds("caption-Close").expect("a close button"); + assert_eq!(close.right(), px(280.0)); +} + +/// A long centered title ends before the Linux round buttons. +#[gpui::test] +fn a_long_centered_title_ends_before_the_buttons(cx: &mut TestAppContext) { + cx.update(Theme::init); + let long = "Ely — Projects at a narrow width, and more besides"; + let (_, cx) = cx.add_window_view(|_, _| Bar(Some(long), crate::theme::Platform::Linux)); + settle(cx); + let title = cx.debug_bounds("titlebar-title").expect("the title"); + let controls = cx.debug_bounds("titlebar-controls").expect("the buttons"); + assert!( + title.right() <= controls.left(), + "{title:?} before {controls:?}" + ); +} + +/// At half the rem size the caption buttons shrink with the bar and stay inside it. +#[gpui::test] +fn caption_buttons_follow_the_windows_rem(cx: &mut TestAppContext) { + cx.update(Theme::init); + let (_, cx) = cx.add_window_view(|_, _| Bar(None, crate::theme::Platform::Windows)); + cx.update(|window, _| window.set_rem_size(px(8.0))); + settle(cx); + let bar = cx.debug_bounds("bar-box").expect("the bar"); + let close = cx.debug_bounds("caption-Close").expect("a close button"); + assert!( + close.top() >= bar.top() && close.bottom() <= bar.bottom(), + "{close:?} inside {bar:?}" + ); +} + +/// A short title sits at the bar's center, whatever stands at either end. +#[gpui::test] +fn a_short_title_sits_at_the_center(cx: &mut TestAppContext) { + cx.update(Theme::init); + let (_, cx) = cx.add_window_view(|_, _| Bar(Some("Notes"), crate::theme::Platform::Linux)); + settle(cx); + let title = cx.debug_bounds("titlebar-title").expect("the title"); + let middle = title.left() + title.size.width / 2.0; + assert!((middle - px(140.0)).abs() < px(1.0), "{title:?} centered"); +} diff --git a/src/theme/tokens.rs b/src/theme/tokens.rs index fdbab9d..cb2cede 100644 --- a/src/theme/tokens.rs +++ b/src/theme/tokens.rs @@ -230,9 +230,9 @@ 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) + /// A Windows caption button's height at a window's rem: the title bar's, less its hairline. + pub fn caption_button_height(&self, rem: Pixels) -> Pixels { + self.titlebar_height().to_pixels(rem) - px(1.0) } /// Resize margin of a client-drawn window.