From 6c6ddcd49384d6ea9f0ee2e63bf7b2643dfd5bcf Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Mon, 10 Aug 2026 04:32:06 +0200 Subject: [PATCH] fix: center tab labels for symmetric highlight padding (#2570) * fix: center tab labels for symmetric highlight padding * docs: note centered tab labels in the changelog * fix: center tab labels by display width, not char count --- docs/next/CHANGELOG.md | 1 + src/ui/tab_surface.rs | 2 +- src/ui/tabs.rs | 65 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 5f13a4a6..0634ac78 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -13,6 +13,7 @@ - The plugin marketplace now discovers valid manifests at repository roots and subdirectories, groups multiple plugins under each repository, and publishes their versions and exact default-branch commits. ### Changed +- Desktop tab labels are now centered in their tabs, so the active-tab highlight has symmetric padding. - Bumped the client/server protocol version to 20 for pane terminal bell forwarding. - Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay. diff --git a/src/ui/tab_surface.rs b/src/ui/tab_surface.rs index 1594f83d..99ca9727 100644 --- a/src/ui/tab_surface.rs +++ b/src/ui/tab_surface.rs @@ -304,7 +304,7 @@ mod tests { assert_eq!(frame.hyperlinks, vec![uri.to_owned()]); assert_eq!( frame_digest(&frame), - "ce383feeaac30922502b7c4f8af53b5ca30e816ec4503ca6d015738b584da487" + "a7c21fa42305a41231c7ae254f264f6ef923f46301d8fc4cd35ab6dfdd651b6b" ); } diff --git a/src/ui/tabs.rs b/src/ui/tabs.rs index c63fc26f..603422fb 100644 --- a/src/ui/tabs.rs +++ b/src/ui/tabs.rs @@ -407,7 +407,14 @@ pub(super) fn render_tab_bar(app: &AppState, frame: &mut Frame, area: Rect) { }; let width = rect.width as usize; let name = tab_chrome_label(ws, idx); - let text = format!(" {:width$}", name, width = width.saturating_sub(1)); + // Pad by terminal columns, not chars, so wide glyphs stay centered. + let padding = width.saturating_sub(display_width_u16(&name) as usize); + let left = padding / 2; + let text = format!( + "{empty:left$}{name}{empty:right$}", + empty = "", + right = padding - left + ); frame.render_widget(Paragraph::new(text).style(style), rect); } @@ -656,6 +663,62 @@ mod tests { assert!(view.new_tab_hit_area.width > 0); } + #[test] + fn cjk_tab_labels_are_centered_by_display_width() { + let mut app = AppState::test_new(); + let mut ws = Workspace::test_new("test"); + ws.tabs[0].set_custom_name("提交 herdr 的反馈".into()); + + app.workspaces = vec![ws]; + app.active = Some(0); + app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); + let view = compute_tab_bar_view(&app.workspaces[0], app.view.tab_bar_rect, 0, true, false); + app.view.tab_hit_areas = view.tab_hit_areas; + + let backend = TestBackend::new(30, 1); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) + .unwrap(); + + // 17 display columns + 4 padding: two columns each side, wide glyphs + // starting right after the left padding. + let rect = app.view.tab_hit_areas[0]; + assert_eq!(rect.width, 21); + let buffer = terminal.backend().buffer(); + assert_eq!(buffer[(rect.x, rect.y)].symbol(), " "); + assert_eq!(buffer[(rect.x + 1, rect.y)].symbol(), " "); + assert_eq!(buffer[(rect.x + 2, rect.y)].symbol(), "提"); + assert_eq!(buffer[(rect.x + rect.width - 2, rect.y)].symbol(), " "); + assert_eq!(buffer[(rect.x + rect.width - 1, rect.y)].symbol(), " "); + } + + #[test] + fn tab_labels_are_centered_in_their_cells() { + let mut app = AppState::test_new(); + let mut ws = Workspace::test_new("test"); + ws.tabs[0].set_custom_name("omarchy".into()); + + app.workspaces = vec![ws]; + app.active = Some(0); + app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); + let view = compute_tab_bar_view(&app.workspaces[0], app.view.tab_bar_rect, 0, true, false); + app.view.tab_hit_areas = view.tab_hit_areas; + + let backend = TestBackend::new(30, 1); + let mut terminal = Terminal::new(backend).unwrap(); + terminal + .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) + .unwrap(); + + let rect = app.view.tab_hit_areas[0]; + let buffer = terminal.backend().buffer(); + let cell: String = (rect.x..rect.x + rect.width) + .map(|x| buffer[(x, rect.y)].symbol()) + .collect(); + assert_eq!(cell, " omarchy "); + } + #[test] fn active_auto_named_tab_keeps_readable_weight() { let mut app = AppState::test_new();