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.
This commit is contained in:
l0ng-ai
2026-09-24 12:51:51 +08:00
parent 458c923aaa
commit e210e00050
8 changed files with 210 additions and 131 deletions
+13 -7
View File
@@ -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 面板**:改动列表是主体。可执行的提交按钮才突出;未填写信息或不可提交时保持次级。
- **设置**:章节标题 > 标签 > 描述;开关/滑块/主操作共享强调色;分隔线弱于控件边界。
- **菜单与命令面板**:亮度抬升、有限阴影;当前键盘行清楚,普通项不着色;小窗口内可滚动。
+51 -10
View File
@@ -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)),
+1
View File
@@ -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",
+1
View File
@@ -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 => "シェル",
+1
View File
@@ -828,6 +828,7 @@ l10n_keys! {
PanelPortsUnsupported,
PanelPortsProbeFailed,
PanelPortsRestricted,
PanelPortsEmpty,
PanelLatency,
PortAutoForwarded,
PanelCwd,
+1
View File
@@ -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 => "工作目录",
+123 -85
View File
@@ -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<gpui_component::input::InputState>,
cx: &mut Context<Self>,
) -> 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<AnyElement>,
cx: &mut Context<Self>,
) -> 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::<crate::ui::presets::Surfaces>().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)),
+19 -29
View File
@@ -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::<crate::ui::presets::Surfaces>().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, &regular, 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