diff --git a/nebula_app/src/gpui_shell/workspace/sidebar.rs b/nebula_app/src/gpui_shell/workspace/sidebar.rs index 48ecbd4..6b1c60a 100644 --- a/nebula_app/src/gpui_shell/workspace/sidebar.rs +++ b/nebula_app/src/gpui_shell/workspace/sidebar.rs @@ -53,6 +53,31 @@ pub(super) fn resting_activity( } impl NebulaWorkspace { + pub(super) fn shell_status_label( + tag: SharedString, + family: SharedString, + size_px: f32, + color: gpui::Hsla, + ) -> gpui::Div { + // As in 1.7, short labels keep their intrinsic width and align right in + // the status row. Only a long label is clipped to the available space. + div() + .max_w_full() + .min_w_0() + .truncate() + .font_family(family) + .text_size(px(size_px)) + .font_weight(FontWeight::NORMAL) + .text_color(color) + .child(tag) + } + + pub(super) fn tab_status_slot(width: f32) -> gpui::Div { + // Nerd Font ink can extend beyond its advance. Keep the 1.7 slot + // unclipped; the enclosing tab still clips at its own outer boundary. + div().relative().w(px(width)).h_full().flex_shrink_0() + } + /// Reserve the shaped shell label's width, capped to leave room for the title. pub(super) fn shell_status_width( window: &Window, @@ -356,16 +381,13 @@ impl NebulaWorkspace { .into_any_element(), ), SidebarActivity::Idle => shell_tag.map(|tag| { - div() - .w_full() - .min_w_0() - .truncate() - .font_family(chrome_family.clone()) - .text_size(px(label_px * SIDEBAR_TAG_SCALE)) - .font_weight(FontWeight::NORMAL) - .text_color(status_color) - .child(tag) - .into_any_element() + Self::shell_status_label( + tag, + chrome_family.clone(), + label_px * SIDEBAR_TAG_SCALE, + status_color, + ) + .into_any_element() }), }; // 三类行位移(旧壳 tab_drag_draw_y 的语义):被拖行骑指针, @@ -565,12 +587,7 @@ impl NebulaWorkspace { ) }) .child( - div() - .relative() - .w(px(status_width)) - .h_full() - .overflow_hidden() - .flex_shrink_0() + Self::tab_status_slot(status_width) .when_some(resting_status, |slot, status| { slot.child( h_flex() @@ -1090,6 +1107,97 @@ impl NebulaWorkspace { mod tests { use super::*; + #[cfg(feature = "gpui-test-support")] + mod layout { + use super::*; + use std::cell::RefCell; + use std::rc::Rc; + + struct StatusProbe { + label: SharedString, + font_size: f32, + mask: Rc>>>, + } + + impl Render for StatusProbe { + fn render( + &mut self, + _window: &mut Window, + _cx: &mut Context, + ) -> impl IntoElement { + let mask = self.mask.clone(); + h_flex() + .id("status-probe-row") + .debug_selector(|| "status-probe-row".to_owned()) + .w(px(200.0)) + .h(px(TAB_ROW_H)) + .px_2() + .overflow_hidden() + .child(div().flex_1()) + .child( + NebulaWorkspace::tab_status_slot(TAB_STATUS_SLOT_W) + .id("status-probe-slot") + .debug_selector(|| "status-probe-slot".to_owned()) + .child( + h_flex().absolute().inset_0().justify_end().items_center().child( + NebulaWorkspace::shell_status_label( + self.label.clone(), + ".SystemUIFont".into(), + self.font_size, + gpui::black(), + ) + .id("status-probe-label") + .debug_selector(|| "status-probe-label".to_owned()), + ), + ) + .child( + canvas( + |_, _, _| {}, + move |_, _, window, _| { + *mask.borrow_mut() = Some(window.content_mask().bounds); + }, + ) + .absolute() + .size_full(), + ), + ) + } + } + + #[gpui::test] + fn short_shells_align_right_long_shells_fit_and_glyph_ink_keeps_row_padding( + cx: &mut gpui::TestAppContext, + ) { + cx.update(gpui_component::init); + for font_size in [12.0, 15.0, 20.0] { + for label in ["sh", "debian", "a-very-long-shell-distribution-name"] { + let mask = Rc::new(RefCell::new(None)); + let recorded = mask.clone(); + let (_, visual) = cx.add_window_view(|_, _| StatusProbe { + label: label.into(), + font_size, + mask, + }); + let row = visual.debug_bounds("status-probe-row").unwrap(); + let slot = visual.debug_bounds("status-probe-slot").unwrap(); + let text = visual.debug_bounds("status-probe-label").unwrap(); + assert!(text.size.width <= slot.size.width); + assert_eq!( + text.right(), + slot.right(), + "shell labels retain the 1.7 right edge" + ); + if label == "sh" { + assert!(text.size.width < slot.size.width, "short labels stay intrinsic"); + } + let clip = recorded.borrow().expect("status paint mask"); + assert!(clip.right() > slot.right(), "glyph overhang must retain row padding"); + assert!(clip.right() <= row.right(), "the outer row still clips overflow"); + } + } + } + } + #[test] fn finished_dot_is_an_event_so_the_tab_you_are_watching_never_shows_it() { // 你正盯着 agent 跑完:那个点在你眼前亮起没有任何信息量。 diff --git a/nebula_app/src/gpui_shell/workspace/top_tabs.rs b/nebula_app/src/gpui_shell/workspace/top_tabs.rs index 438e99f..eddb08f 100644 --- a/nebula_app/src/gpui_shell/workspace/top_tabs.rs +++ b/nebula_app/src/gpui_shell/workspace/top_tabs.rs @@ -238,16 +238,13 @@ impl NebulaWorkspace { .into_any_element(), ), SidebarActivity::Idle => shell_tag.map(|tag| { - div() - .w_full() - .min_w_0() - .truncate() - .font_family(chrome_family.clone()) - .text_size(px(label_px * 0.8)) - .font_weight(FontWeight::NORMAL) - .text_color(status_color) - .child(tag) - .into_any_element() + Self::shell_status_label( + tag, + chrome_family.clone(), + label_px * 0.8, + status_color, + ) + .into_any_element() }), }; let strip = color.map(|color| gpui::Rgba { @@ -462,12 +459,7 @@ impl NebulaWorkspace { ) }) .child( - div() - .relative() - .w(px(status_width)) - .h_full() - .overflow_hidden() - .flex_shrink_0() + Self::tab_status_slot(status_width) .when_some(resting_status, |slot, status| { slot.child( h_flex()