mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-05 08:02:13 +00:00
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:
+13
-7
@@ -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
@@ -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)),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 => "シェル",
|
||||
|
||||
@@ -828,6 +828,7 @@ l10n_keys! {
|
||||
PanelPortsUnsupported,
|
||||
PanelPortsProbeFailed,
|
||||
PanelPortsRestricted,
|
||||
PanelPortsEmpty,
|
||||
PanelLatency,
|
||||
PortAutoForwarded,
|
||||
PanelCwd,
|
||||
|
||||
@@ -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
@@ -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
@@ -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, ®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
|
||||
|
||||
Reference in New Issue
Block a user