Theme · Platform: a title bar in three parts, buttons at the window's rem
A title bar lays out a start, a title and an end. In the centered styles the start and end grow alike from nothing, their padding inside them, so a short title sits at the bar's center and a long one gives way before whatever stands at either end; on Windows the title, or a spacer when there is none, takes the free width before the buttons. Caption buttons take the bar's height at the window's own rem. Tests keep an untitled Windows bar's buttons at its end, a long Linux title before the round buttons, a short title centered, and the buttons inside a bar at rem 8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
815aed304d
commit
ba865710a7
4 files changed
+115
-30
No files matched your search
@@ -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 |
|
||||
+40
-27
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Self>) -> 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");
|
||||
}
|
||||
+3
-3
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user