From e210e00050e13bd369266b0cf72bec78ee7b5255 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 24 Sep 2026 12:51:51 +0800 Subject: [PATCH] feat(ui): restyle the right panel after the v4 design - Tab row: 12.5/16rem word tabs 22px in and 18px apart, the current one in body ink at medium weight; no hover pill, no underline bar, no hairline under the row. - Info: Session, Processes and Ports are spaced 16px apart with no rules; 28px medium muted headings, 28px Session rows on a 76px label floor with values in body ink, 26px process rows with a tree elbow for children, and an explicit empty line for Ports. - Files: the search sits in a 28px filled well; tree rows are 26px with a disclosure chevron column, ignored entries dim their icon instead of going italic, and a folder's change dot is 5px. - docs/design-system.md updated to match. --- docs/design-system.md | 20 ++-- src/ui/file_tree.rs | 61 +++++++++++-- src/ui/i18n/en.rs | 1 + src/ui/i18n/ja.rs | 1 + src/ui/i18n/mod.rs | 1 + src/ui/i18n/zh.rs | 1 + src/ui/right_panel.rs | 208 +++++++++++++++++++++++++----------------- src/ui/tab_strip.rs | 48 ++++------ 8 files changed, 210 insertions(+), 131 deletions(-) diff --git a/docs/design-system.md b/docs/design-system.md index 2b07c5ea..b0ec3392 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -13,7 +13,7 @@ 5. **异常**:失败、冲突、需要处理的警告。局部状态色,加文字或形状,不单靠颜色。 同屏不能把所有选中项都画成主按钮。当前标签页用中性灰底 + semibold;右侧面板当前页签用 -正文色文字 + 底部 2px 横条;真正可执行的主按钮才使用实色强调色。每个任务区只突出一个主操作。 +正文色文字 + medium 字重;真正可执行的主按钮才使用实色强调色。每个任务区只突出一个主操作。 ## 颜色角色 @@ -44,7 +44,7 @@ - Rest:中性、稳定。工具按钮不因鼠标移出而消失。 - Hover:轻微中性填充,只用于可交互目标。设置说明行不整体发亮。 -- Selected:表达持久的当前位置。侧栏当前行用中性底 + semibold,右侧页签用文字色 + 底部横条。 +- Selected:表达持久的当前位置。侧栏当前行用中性底 + semibold,右侧页签用正文色 + medium 字重,不画横条也不画底。 - Keyboard focus:清晰的强调色焦点环/当前行,不能仅靠 hover 表达。 - Pressed:同一色系的短暂加深,不能跳到另一个主题色。 - Disabled:保留边界与标签;降低对比,不能像可执行的主按钮。 @@ -65,8 +65,7 @@ - 右侧面板和侧栏的紧凑标题不再大写,靠字重与颜色区分层级。 - 以 4px 为间距基础:紧密内容 4–8,行内 8–12,区块 24–32。列表外边距与文字预算同步。 - 形状:侧栏标签页行 `CARD_RADIUS`,侧栏搜索框 `rounded_lg`,工作区切换 `rounded_md`, - 图标按钮圆角方形(`rounded_lg` / 小号 4px);右侧面板文字页签的 hover 底为胶囊 - (`rounded_full`)。浮层 `POPOVER_RADIUS` 10px。 + 图标按钮圆角方形(`rounded_lg` / 小号 4px);右侧面板文字页签没有 hover 底,只变文字色。浮层 `POPOVER_RADIUS` 10px。 - 侧栏单行标签页行高 28px,头像 18px 圆形品牌底;搜索框与工作区切换同为 28px。 ## 各界面约束 @@ -74,10 +73,17 @@ - **侧栏**:名称优先于分支和 diff 数量;当前项清晰,品牌头像不形成彩色墙。分组标题用正文色 semibold,分支与 diff 数量右对齐作次级信息;展开的分组不画 chevron,折叠后才显示。 - **侧栏顶部**:导航与内容用留白衔接,不加贯穿的 header 底部分隔线;左右侧栏保持连续表面。 -- **右侧面板页签**:文字页签(信息 / 更改 / 文件),当前页签正文色 + 底部 2px 横条落在 - 页签栏底部的细分隔线上;「更改」后附改动数。 +- **右侧面板页签**:文字页签(信息 / 更改 / 文件),12.5/16rem,首个页签距左缘 22px、 + 页签间距 18px;当前页签正文色 + medium,其余辅助色,hover 只把文字提到正文色。页签栏 + 下方不画横条、不画分隔线。「更改」后附改动数(辅助色、regular)。尾部是面板开关与「更多」。 +- **信息页**:Session / Processes / Ports 三节,节间 16px、不画分隔线;节标题 28px 高、 + 11.5/16rem medium 辅助色。Session 为两列(标签列至少 76px,行高 28px),值用正文色; + cwd 的父路径弱化、末级正文色,从头部省略。进程行 26px,子进程以 `└` 缩进,PID 辅助色 + mono。Ports 标题尾部带「+」转发按钮,空状态一行辅助色说明。 - **右侧文字层级**:文件树名称使用侧栏标题色,不能整体降为辅助灰;当前项使用 selected 文字色与 medium 字重,根目录以字重区分。Git 普通文件名使用 resting 文字色与界面字体,路径、数量与说明保持次级;信息页的路径前缀弱化,末级名称突出。 -- **文件树**:文件名中性;A/M/? 等尾部字符承载 Git 状态;目录用小点表示子项变化;冲突才整行提升。 +- **文件树**:顶部是 28px 的浅底搜索框(圆角 7)。行高 26px、圆角 6;目录前有展开 chevron, + 文件留空同宽;文件名中性;A/M/? 等尾部字符承载 Git 状态;目录用 5px 小点表示子项变化; + 忽略项文字辅助色、图标半透明,不用斜体;冲突才整行提升。 - **Git 面板**:改动列表是主体。可执行的提交按钮才突出;未填写信息或不可提交时保持次级。 - **设置**:章节标题 > 标签 > 描述;开关/滑块/主操作共享强调色;分隔线弱于控件边界。 - **菜单与命令面板**:亮度抬升、有限阴影;当前键盘行清楚,普通项不着色;小窗口内可滚动。 diff --git a/src/ui/file_tree.rs b/src/ui/file_tree.rs index e290a5ff..6d14f3b7 100644 --- a/src/ui/file_tree.rs +++ b/src/ui/file_tree.rs @@ -38,6 +38,19 @@ use gpui_component::{ // glyph sits right there to compare against. const INDENT: f32 = 14.0; +/// How tall a tree row is. +const TREE_ROW_H: f32 = 26.; + +/// The disclosure chevron's column, drawn on folders and left empty on files +/// so every name at one depth starts at the same x. +const CHEVRON_W: f32 = 10.; + +/// The gap between a row's chevron, icon and name. +const TREE_GAP: f32 = 6.; + +/// Where a row's label starts, measured from the row's own inset. +const LABEL_LEAD: f32 = CHEVRON_W + TREE_GAP + ROW_GLYPH + TREE_GAP; + const REFRESH_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(200); const SEARCH_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(200); @@ -1779,8 +1792,8 @@ impl Tty7App { // Aligned with the label column of a real row at this // depth: ROW_INSET for the row's own inset, INDENT for the // depth, then the width of the icon and its gap. - .pl(px(ROW_INSET + row.depth as f32 * INDENT + 20.0)) - .py_1() + .pl(px(ROW_INSET + row.depth as f32 * INDENT + LABEL_LEAD)) + .h(px(TREE_ROW_H)) .items_center() .text_xs() .italic() @@ -1853,7 +1866,9 @@ impl Tty7App { cx.theme().sidebar_foreground }) .when(selected, |d| d.font_weight(gpui::FontWeight::MEDIUM)) - .when(row.entry.ignored, |d| d.italic().text_color(muted)) + // Ignored entries recede — dimmer ink and a half-strength icon — + // but stay upright: they are still files you can open. + .when(row.entry.ignored, |d| d.text_color(muted)) // Ordinary changes belong in the status badge. Only a conflict // should turn an entire filename into an attention signal. .when_some(deco.tint, |d, status| { @@ -1868,24 +1883,50 @@ impl Tty7App { .into_any_element() }; + let chevron = is_dir.then(|| { + Icon::new(match row.expanded || row.is_root { + true => IconName::ChevronDown, + false => IconName::ChevronRight, + }) + .size(px(CHEVRON_W)) + .text_color(muted) + }); let row_el = h_flex() .id(SharedString::from(format!("tree-{}", path.display()))) .items_center() - .gap_1() + .gap(px(TREE_GAP)) + .h(px(TREE_ROW_H)) .pl(px(ROW_INSET + row.depth as f32 * INDENT)) .pr(px(ROW_INSET)) - .py_1() - .rounded(cx.theme().radius) + .rounded(px(6.)) .cursor_pointer() .when(selected, |d| d.bg(gpui::rgb(sf.selected))) .when(!selected, |d| d.hover(|s| s.bg(gpui::rgb(sf.hover)))) - .child(Icon::new(icon).size(px(ROW_GLYPH)).text_color(muted)) + .child( + div() + .flex_none() + .w(px(CHEVRON_W)) + .flex() + .justify_center() + .children(chevron), + ) + .child( + div() + .flex_none() + .flex() + .justify_center() + .when(row.entry.ignored, |d| d.opacity(0.5)) + .child(Icon::new(icon).size(px(ROW_GLYPH)).text_color(muted)), + ) .child(label) + // A folder with work under it says so with one small dot in the + // tint of the change — amber for ordinary edits, red for a + // conflict — rather than a letter it cannot honestly carry. .when(is_dir && deco.tint.is_some(), |d| { d.child( div() .flex_none() - .size(px(4.)) + .size(px(5.)) .rounded_full() .bg(status_color(deco.tint.unwrap(), cx)), ) @@ -2322,7 +2363,7 @@ fn order_innermost_first(decor: &mut Decorations) { /// One hash probe per row and no allocation on the path that matters. fn row_decoration(decor: &Decorations, entry: &TreeEntry) -> RowDeco { - // A gitignored row keeps the italic-and-dim it has always worn and takes + // A gitignored row keeps the dimmed ink and icon it already wears and takes // nothing else: a letter and a colour would be describing a file the // repository is not tracking. if entry.ignored { @@ -2709,7 +2750,7 @@ mod tests { assert_eq!( row_decoration(&decor, &tree_entry("/repo/target/debug/app", false, true)), RowDeco::default(), - "italic and dim is the whole of what an ignored row says" + "dimmed ink and icon are the whole of what an ignored row says" ); assert_eq!( row_decoration(&decor, &tree_entry("/repo/target", true, true)), diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index d0e8621a..ab08c3f3 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -1127,6 +1127,7 @@ pub fn translate_en(key: L10nKey) -> &'static str { L10nKey::PanelPortsRestricted => { "Something here runs as another user, whose ports aren't visible." } + L10nKey::PanelPortsEmpty => "No forwarded ports", L10nKey::PanelLatency => "latency", L10nKey::PortAutoForwarded => "Remote :{port} is now http://localhost:{local}", L10nKey::PanelCwd => "cwd", diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 56470abf..e2d62961 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -1199,6 +1199,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> { L10nKey::PanelPortsRestricted => { "他のユーザーで動いているプロセスがあり、そのポートは見えません。" } + L10nKey::PanelPortsEmpty => "転送中のポートはありません", L10nKey::PortAutoForwarded => "リモートの :{port} は http://localhost:{local} で開けます", L10nKey::PanelCwd => "作業ディレクトリ", L10nKey::PanelShell => "シェル", diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index c3b97bb1..28bde4d4 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -828,6 +828,7 @@ l10n_keys! { PanelPortsUnsupported, PanelPortsProbeFailed, PanelPortsRestricted, + PanelPortsEmpty, PanelLatency, PortAutoForwarded, PanelCwd, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 82d96fb2..fade261c 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -1069,6 +1069,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> { L10nKey::PanelLatency => "延迟", L10nKey::PanelPortsUnsupported => "对端的 tty7-server 太旧,列不出端口。", L10nKey::PanelPortsProbeFailed => "没能查出这个窗格在监听什么。", + L10nKey::PanelPortsEmpty => "没有转发的端口", L10nKey::PanelPortsRestricted => "这里有以其他用户身份运行的进程,看不到它们的端口。", L10nKey::PortAutoForwarded => "远程 :{port} 现在是 http://localhost:{local}", L10nKey::PanelCwd => "工作目录", diff --git a/src/ui/right_panel.rs b/src/ui/right_panel.rs index 994f83c0..17a00739 100644 --- a/src/ui/right_panel.rs +++ b/src/ui/right_panel.rs @@ -32,9 +32,9 @@ fn right_panel_tabs_floor(window: &Window, cx: &gpui::App) -> f32 { (TAB_ROW_LEAD + crate::ui::tab_strip::right_panel_tab_labels_w(window, cx) + chrome).ceil() } -/// The tab row's leading inset, and the 2px gaps it puts between the three -/// tabs, the spacer and the chrome. -const TAB_ROW_LEAD: f32 = 4.; +/// The tab row's leading inset: the first label lands 22px in, and each tab's +/// click target already reaches `TAB_OUTER_PAD` past its label. +const TAB_ROW_LEAD: f32 = 22. - crate::ui::tab_strip::TAB_OUTER_PAD; /// How tall the tab row is where it sits below the title bar (Windows and /// Linux): the tab's 26px hover pill plus 2px either side. A full title-bar @@ -67,8 +67,32 @@ pub(crate) const TEXT_MONO: f32 = TEXT - STEP; pub(crate) const META: f32 = 12. * STEP; pub(crate) const META_MONO: f32 = META - STEP; -/// Compact section headings share the sidebar group-label size. -pub(crate) const HEADING: f32 = META; +/// Compact section headings: a half step under the caption size, set apart by +/// weight and ink rather than by size. +pub(crate) const HEADING: f32 = 11.5 * STEP; + +/// The word tabs at the top of the panel, a half step under body text. +pub(crate) const TAB_TEXT: f32 = 12.5 * STEP; + +/// How tall a Session, section-heading or search row is. +const INFO_ROW_H: f32 = 28.; + +/// How tall a process row is — a notch tighter than the Session rows, since +/// the list is read as one block rather than line by line. +const PROC_ROW_H: f32 = 26.; + +/// How far a child process sits in from its parent, the width of the `└` +/// elbow that marks it. +const PROC_INDENT: f32 = 14.; + +/// The space between two sections of the Info tab. +const SECTION_GAP: f32 = 16.; + +/// The Session label column's floor, gap included, at the default 16px rem. +const INFO_LABEL_MIN: f32 = 76.; + +/// The space between a Session label and its value. +const INFO_LABEL_GAP: f32 = 9.; /// The leading glyph on a panel row — the file tree's folder and file marks. /// @@ -360,8 +384,11 @@ fn info_label_column(rows: &[InfoRow], window: &mut Window, cx: &gpui::App) -> g // they are carried as multiples of it rather than as pixels — otherwise // raising `ui_font_size` grows the labels into a clamp fitted to a // smaller face, and every one of them wraps. - let label_px = TEXT * window.rem_size().as_f32(); - let min = 46. / 12. * label_px; + let rem = window.rem_size().as_f32(); + let label_px = TEXT * rem; + // The column is 76px at the default rem — room for "changes" and the + // gap after it — and grows with the interface size. + let min = INFO_LABEL_MIN / 16. * rem - INFO_LABEL_GAP; let max = 108. / 12. * label_px; let font = gpui::Font { family: cx.theme().font_family.clone(), @@ -500,10 +527,6 @@ impl Tty7App { .items_center() .gap(px(2.)) .pl(px(TAB_ROW_LEAD)) - // The tab bar is a strip of labels over a body, so it - // closes on a hairline the current tab's bar sits on. - .border_b_1() - .border_color(cx.theme().sidebar_border) .relative() .children(self.right_panel_tabs( width @@ -517,7 +540,7 @@ impl Tty7App { // Navigation controls stay visible on both sidebars. .child(self.window_chrome(window, cx)) })) - .children(cfg!(target_os = "macos").then(|| div().flex_none().h(px(10.)))) + .children(cfg!(target_os = "macos").then(|| div().flex_none().h(px(8.)))) .child(body) .children(self.sftp_transfers_footer(cx)) .child(handle) @@ -667,12 +690,8 @@ impl Tty7App { })) // Where the tabs live in this row, they are the heading: the // current one already names the panel, and the Changes tab carries - // the count. The row closes on the hairline their bar sits on. - .when(tabs.is_some(), |row| { - row.pl(px(TAB_ROW_LEAD)) - .border_b_1() - .border_color(cx.theme().sidebar_border) - }) + // the count. No rule under it — the body's own inset is the break. + .when(tabs.is_some(), |row| row.pl(px(TAB_ROW_LEAD))) .when(tabs.is_none(), |row| { row.child( h_flex() @@ -708,11 +727,8 @@ impl Tty7App { this.child( h_flex() .flex_shrink_0() - // Full height, so the current tile's underline — pinned - // to the bottom of its own box — lands on the rule that - // closes this row, the way it does on macOS. Without it - // the tiles are only as tall as a glyph and the bar - // floats a few pixels above the line. + // Full height, so each tab's click target is the + // whole row, not a glyph-tall strip in its middle. .h_full() .items_center() .gap(px(2.)) @@ -729,29 +745,43 @@ impl Tty7App { input: &gpui::Entity, cx: &mut Context, ) -> AnyElement { + // The strip keeps `SEARCH_H` so it still lines up with the Source + // Control commit box beside it; the field inside is a 28px well on the + // faint fill, the same shape as the sidebar's search. h_flex() .flex_none() .items_center() - // 8 here plus the `.xsmall()` field's own 4px of leading padding - // is 12px of daylight between the glyph and the first character. - .gap(px(8.)) .h(px(SEARCH_H)) - .px(px(CONTENT_INSET)) + .px(px(CONTENT_INSET - ROW_INSET)) .child( - Icon::new(IconName::Search) - .small() - .text_color(cx.theme().muted_foreground), - ) - .child( - div() + h_flex() .flex_1() .min_w_0() - // A filter with no way out of it but selecting the text - // and deleting it is a filter people leave on and then - // wonder where their files went. The button only exists - // while there is something to clear, so an empty field - // still reads as one line of chrome. - .child(Input::new(input).appearance(false).xsmall().cleanable(true)), + .items_center() + // 7 here plus the `.xsmall()` field's own 4px of leading + // padding is 11px of daylight between glyph and text. + .gap(px(7.)) + .h(px(INFO_ROW_H)) + .px(px(ROW_INSET + 2.)) + .rounded(px(7.)) + .bg(cx.theme().muted) + .child( + Icon::new(IconName::Search) + .size(px(12.)) + .text_color(cx.theme().muted_foreground), + ) + .child( + div() + .flex_1() + .min_w_0() + // A filter with no way out of it but selecting the + // text and deleting it is a filter people leave on + // and then wonder where their files went. The + // button only exists while there is something to + // clear, so an empty field still reads as one line + // of chrome. + .child(Input::new(input).appearance(false).xsmall().cleanable(true)), + ), ) .into_any_element() } @@ -910,14 +940,19 @@ impl Tty7App { // Rows pad themselves back out to `CONTENT_INSET`, so their hover fill // bleeds past the text on both sides — the geometry the Source Control // tab's rows are on, one tab over. - let mut list = v_flex().px(px(CONTENT_INSET - ROW_INSET)).py(px(2.)); + let mut list = v_flex().px(px(CONTENT_INSET - ROW_INSET)); for (i, row) in rows.into_iter().enumerate() { list = list.child(self.info_row(i, row, label_w, cx)); } let inner = v_flex() - .child(self.panel_subtitle(t(L10nKey::PanelSessionSubtitle), false, None, cx)) - .child(list) + .pb(px(SECTION_GAP)) + .gap(px(SECTION_GAP)) + .child( + v_flex() + .child(self.panel_subtitle(t(L10nKey::PanelSessionSubtitle), None, cx)) + .child(list), + ) .children(self.procs_section(pane_id, cx)) .children(self.ports_section(ctx.as_ref(), cx)) .into_any_element(); @@ -963,7 +998,7 @@ impl Tty7App { .flex_1() .min_w_0() .text_size(rems(TEXT)) - .text_color(cx.theme().sidebar_foreground) + .text_color(cx.theme().foreground) .child( div() .min_w_0() @@ -972,14 +1007,7 @@ impl Tty7App { .text_color(cx.theme().muted_foreground) .child(head), ) - .child( - div() - .min_w_0() - .flex_shrink(1.) - .truncate() - .font_weight(gpui::FontWeight::MEDIUM) - .child(leaf), - ) + .child(div().min_w_0().flex_shrink(1.).truncate().child(leaf)) .into_any_element() } InfoValue::Text(v) => div() @@ -987,7 +1015,7 @@ impl Tty7App { .min_w_0() .truncate() .text_size(rems(TEXT)) - .text_color(cx.theme().sidebar_foreground) + .text_color(cx.theme().foreground) .child(v) .into_any_element(), InfoValue::Diff { @@ -1100,10 +1128,10 @@ impl Tty7App { .id(id.clone()) .group(id) .relative() - .items_baseline() - .gap(px(9.)) + .items_center() + .gap(px(INFO_LABEL_GAP)) + .h(px(INFO_ROW_H)) .px(px(ROW_INSET)) - .py(px(4.)) .rounded(crate::ui::rounding::ROW_RADIUS) .text_size(rems(TEXT)) // Only rows that can do something light up, so the fill is never a @@ -1146,39 +1174,31 @@ impl Tty7App { .tooltip(tooltip) } + /// A section heading of the Info tab: a 28px row, the label a half step + /// under captions, medium and muted. Sections are told apart by the space + /// between them, not by rules, so there is no divider to ask for. pub(crate) fn panel_subtitle( &self, text: &str, - divider: bool, trailing: Option, cx: &mut Context, ) -> AnyElement { h_flex() - .when(divider, |d| { - d.mt(px(6.)) - .border_t_1() - .border_color(cx.theme().sidebar_border) - }) + .flex_none() + .h(px(INFO_ROW_H)) .items_center() .justify_between() .pl(px(CONTENT_INSET)) .pr(px(if trailing.is_some() { - CONTENT_INSET - crate::ui::app::TILE_PAD + CONTENT_INSET - crate::ui::app::TILE_PAD_SM } else { CONTENT_INSET })) - .pt(px(match (divider, trailing.is_some()) { - (true, false) => 12., - (true, true) => 8., - (false, false) => 10., - (false, true) => 6., - })) - .pb(px(if trailing.is_some() { 0. } else { 4. })) .child( // Weight and ink, not capitals, set a compact heading apart. div() .text_size(rems(HEADING)) - .font_weight(gpui::FontWeight::SEMIBOLD) + .font_weight(gpui::FontWeight::MEDIUM) .text_color(cx.theme().muted_foreground) .child(text.to_string()), ) @@ -1192,30 +1212,46 @@ impl Tty7App { return None; } let mono = cx.theme().mono_font_family.clone(); - let mut list = v_flex().px(px(CONTENT_INSET)).py(px(1.)).gap(px(2.)); + let hover = gpui::rgb(cx.global::().sidebar.hover); + let mut list = v_flex().px(px(CONTENT_INSET - ROW_INSET)); for p in procs { + let depth = f32::from(p.depth); list = list.child( h_flex() + .h(px(PROC_ROW_H)) .items_center() - .gap(px(8.)) + .gap(px(6.)) + .px(px(ROW_INSET)) + .rounded(crate::ui::rounding::ROW_RADIUS) + .hover(move |s| s.bg(hover)) + // A child hangs off its parent by an elbow in the last + // column of its indent; the root has neither. + .when(p.depth > 0, |row| { + row.child( + div() + .flex_none() + .w(px(depth * PROC_INDENT)) + .flex() + .justify_end() + .pr(px(2.)) + .text_size(rems(META)) + .text_color(cx.theme().muted_foreground) + .child("└"), + ) + }) .child( div() .flex_1() .min_w_0() .truncate() - .pl(px(f32::from(p.depth) * 10.)) - .text_size(rems(TEXT_MONO)) - .font_family(mono.clone()) + .text_size(rems(TEXT)) // Which of these has the terminal is the one thing // the list is read for, and a hue apart from its // neighbours was carrying it alone — a difference // a light theme flattens and colour vision can // miss. Weight says it a second way. - .when(p.foreground, |d| { - d.font_weight(gpui::FontWeight::MEDIUM) - .text_color(cx.theme().foreground) - }) - .when(!p.foreground, |d| d.text_color(cx.theme().muted_foreground)) + .text_color(cx.theme().foreground) + .when(p.foreground, |d| d.font_weight(gpui::FontWeight::MEDIUM)) .child(p.name.clone()), ) .child( @@ -1230,7 +1266,7 @@ impl Tty7App { } Some( v_flex() - .child(self.panel_subtitle(t(L10nKey::PanelProcessesSubtitle), true, None, cx)) + .child(self.panel_subtitle(t(L10nKey::PanelProcessesSubtitle), None, cx)) .child(list) .into_any_element(), ) @@ -1360,8 +1396,8 @@ impl Tty7App { .relative() .items_center() .gap(px(8.)) + .h(px(PROC_ROW_H)) .px(px(ROW_INSET)) - .py(px(1.)) .rounded(crate::ui::rounding::ROW_RADIUS) .hover(|s| s.bg(gpui::rgb(sf.hover))) .child(info_chip( @@ -1429,7 +1465,7 @@ impl Tty7App { Some( v_flex() - .child(self.panel_subtitle(t(L10nKey::PanelPortsSubtitle), true, add, cx)) + .child(self.panel_subtitle(t(L10nKey::PanelPortsSubtitle), add, cx)) .when( ports.is_empty() && forwards.is_empty() && !form_open, |this| { @@ -1446,12 +1482,14 @@ impl Tty7App { (true, _) => L10nKey::PanelPortsUnsupported, (false, PortProbe::Unavailable(_)) => L10nKey::PanelPortsProbeFailed, (false, PortProbe::Restricted) => L10nKey::PanelPortsRestricted, - (false, PortProbe::Ok) => L10nKey::None, + (false, PortProbe::Ok) => L10nKey::PanelPortsEmpty, }; this.child( div() .px(px(CONTENT_INSET)) - .py(px(2.)) + .min_h(px(PROC_ROW_H)) + .flex() + .items_center() .text_size(rems(TEXT)) .text_color(cx.theme().muted_foreground) .child(t(key)), diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index a7d38484..82edcaf7 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -607,7 +607,7 @@ const RIGHT_PANEL_TABS: [(RightPanelTab, L10nKey); 3] = [ ]; fn right_panel_tab_size(window: &Window) -> f32 { - window.rem_size().as_f32() * crate::ui::right_panel::META + window.rem_size().as_f32() * crate::ui::right_panel::TAB_TEXT } fn right_panel_tab_font(cx: &gpui::App) -> gpui::Font { @@ -615,7 +615,8 @@ fn right_panel_tab_font(cx: &gpui::App) -> gpui::Font { family: cx.theme().font_family.clone(), features: Default::default(), fallbacks: None, - weight: FontWeight::SEMIBOLD, + // The current tab's weight, which is the widest any label is drawn at. + weight: FontWeight::MEDIUM, style: Default::default(), } } @@ -635,10 +636,11 @@ pub(crate) fn right_panel_tab_labels_w(window: &Window, cx: &gpui::App) -> f32 { .sum() } -/// Right panel tab geometry: the gap between two tabs' hover pills, the pill's -/// own inset around its label, and the gap before the Changes count. -const TAB_OUTER_PAD: f32 = 2.; -const TAB_INNER_PAD: f32 = 8.; +/// Right panel tab geometry: each label's click target reaches half the 18px +/// gap to its neighbour, there is no pill inside it, and the Changes count +/// hangs 5px off its label. +pub(crate) const TAB_OUTER_PAD: f32 = 9.; +const TAB_INNER_PAD: f32 = 0.; const TAB_COUNT_GAP: f32 = 5.; /// How wide the two chrome tiles at the trailing end of the title bar are, with @@ -1212,7 +1214,6 @@ impl Tty7App { .and_then(|repo| crate::terminal::git_data::status_of(cx, repo.host, &repo.root)) .map(|status| status.entries.len()) .filter(|n| *n > 0); - let hover = gpui::rgb(cx.global::().sidebar.hover); let size = right_panel_tab_size(window); let font = right_panel_tab_font(cx); let ts = window.text_system(); @@ -1224,16 +1225,18 @@ impl Tty7App { }; labels_w + TAB_COUNT_GAP + measure_text(ts, ®ular, size, &n.to_string()) <= avail }); + let body_ink = cx.theme().foreground; RIGHT_PANEL_TABS .into_iter() .map(|(tab, label_key)| { let current = active_tab == tab; // Words, not glyphs: three tabs is few enough to name, and a name // is what a new user has to guess at when the tab is an icon. The - // current one is told apart by ink and the bar under it, both - // neutral — this is secondary navigation, not an action. + // current one is told apart by ink and weight alone — this is + // secondary navigation, not an action, so it gets neither a + // pill nor a bar. let ink = match current { - true => cx.theme().foreground, + true => body_ink, false => cx.theme().muted_foreground, }; let count = match tab { @@ -1246,11 +1249,7 @@ impl Tty7App { // the tabs sit in. .occlude() .flex_shrink_0() - // Full height and `relative` so the bar below can be pinned to - // the row's own bottom edge, where it lands on the hairline - // that closes the row rather than floating under the label. .h_full() - .relative() .flex() .items_center() .px(px(TAB_OUTER_PAD)) @@ -1258,15 +1257,16 @@ impl Tty7App { .child( h_flex() .flex_shrink_0() - .h(px(crate::ui::app::TILE_SIZE_SM + 2.)) .px(px(TAB_INNER_PAD)) .gap(px(TAB_COUNT_GAP)) .items_center() - .rounded_full() - .hover(|s| s.bg(hover)) - .text_size(gpui::rems(crate::ui::right_panel::META)) - .font_weight(FontWeight::SEMIBOLD) + .text_size(gpui::rems(crate::ui::right_panel::TAB_TEXT)) + .font_weight(match current { + true => FontWeight::MEDIUM, + false => FontWeight::NORMAL, + }) .text_color(ink) + .hover(move |s| s.text_color(body_ink)) .child(div().flex_shrink_0().child(t(label_key))) .when_some(count, |row, n| { row.child( @@ -1278,16 +1278,6 @@ impl Tty7App { ) }), ) - .child( - div() - .absolute() - .bottom(px(-1.)) - .left(px(TAB_OUTER_PAD + TAB_INNER_PAD)) - .right(px(TAB_OUTER_PAD + TAB_INNER_PAD)) - .h(px(2.)) - .rounded_full() - .when(current, |bar| bar.bg(cx.theme().foreground)), - ) // Another tab switches to it; the current one puts the panel // away, the way an activity bar behaves everywhere else. // (These only exist while the panel is open, so