mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-01 16:01:56 +00:00
Merge pull request #946 from l0ng-ai/feat/v5-ui-restyle
feat(ui): restyle the workspace after the v5 design
This commit is contained in:
+16
-9
@@ -12,22 +12,22 @@
|
||||
4. **说明与元数据**:路径、分支、数量、快捷键、设置描述。次级文字,不用饱和色抢正文。
|
||||
5. **异常**:失败、冲突、需要处理的警告。局部状态色,加文字或形状,不单靠颜色。
|
||||
|
||||
同屏不能把所有选中项都画成主按钮。工作区使用淡强调色;右侧面板当前页签使用中性底;
|
||||
真正可执行的主按钮才使用实色强调色。每个任务区只突出一个主操作。
|
||||
同屏不能把所有选中项都画成主按钮。当前标签页用中性灰底 + semibold;右侧面板当前页签用
|
||||
正文色文字 + 底部 2px 横条;真正可执行的主按钮才使用实色强调色。每个任务区只突出一个主操作。
|
||||
|
||||
## 颜色角色
|
||||
|
||||
| 角色 | 使用位置 | 规则 |
|
||||
| --- | --- | --- |
|
||||
| `Neutrals.background` | 工作内容、设置内容、输入框 | 安静的基础表面。输入框未聚焦时不能像已选中列表行 |
|
||||
| `Neutrals.sidebar` | 两侧导航 | 与内容轻微分层;用细分隔线补充边界 |
|
||||
| `Neutrals.sidebar` | 两侧导航 | 与内容同色,连成一整块表面;只用细分隔线划界 |
|
||||
| `Neutrals.popover` | 菜单、命令面板、对话框 | 浅色下保持明亮,深色下抬高亮度;只给浮层阴影 |
|
||||
| `foreground` | 正文、标题、当前项 | 正常文字至少 4.5:1;不把普通正文全部加粗 |
|
||||
| `muted_foreground` | 描述、路径、快捷键 | 与主文字明确区分,但仍保持可读性 |
|
||||
| `border` / `divider` | 控件边界 / 区域分隔 | 控件边界较强,区域分隔较弱;不能把页面画成表格线框 |
|
||||
| `Interactions.input_border` | 输入框与普通按钮轮廓 | 对应组件库的 `input` 边框角色,不能误用内容背景色导致边界消失 |
|
||||
| `Interactions.primary` | 保存、连接、有效的提交等主操作 | 强调色实底,成对配置前景色;hover/pressed 保持标签对比度 |
|
||||
| `Interactions.navigation` | 当前标签页、设置导航 | 低饱和淡色底 + 清晰文字;不使用主按钮实色 |
|
||||
| `Surfaces.sidebar.selected` | 当前标签页 | 中性 selected 阶 + semibold;不用强调色,蓝色留给可按的东西 |
|
||||
| `Interactions.choice` | 菜单、命令面板的键盘当前行 | 强调色浅底,区别于鼠标经过的中性 hover |
|
||||
| 中性 `secondary` | 面板页签、分段选项、普通按钮 | 次级导航不能与主要操作争夺注意力 |
|
||||
| `Semantics.danger` | 失败、Git 冲突、破坏性动作 | 红;必须同时有文字/图标/确认语义 |
|
||||
@@ -35,7 +35,7 @@
|
||||
| `Semantics.success` | 成功、Git 新增标记 | 绿;不用于装饰性按钮 |
|
||||
| `Semantics.info/link` | 信息、可点击链接 | 使用当前主题的强调色,与焦点指示同族 |
|
||||
|
||||
默认浅色以白/浅灰为底,蓝色 `#007AFF` 为强调色。
|
||||
默认浅色以纯白为底,正文 `#0F1419`,蓝色 `#1F6BF0` 为强调色(光标同强调色)。
|
||||
默认深色以 `#191B20` 为内容底,`#E2E5EB` 为正文,`#78A8F5` 为强调色。
|
||||
其他主题保留自己的背景与强调色,但红/绿/琥珀的**状态含义**不随 ANSI 槽位变化。
|
||||
所有文字、状态色和主按钮前景色须在实际背景上验证对比度。
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
- Rest:中性、稳定。工具按钮不因鼠标移出而消失。
|
||||
- Hover:轻微中性填充,只用于可交互目标。设置说明行不整体发亮。
|
||||
- Selected:表达持久的当前位置。工作区用淡强调色,辅助页签用中性底。
|
||||
- Selected:表达持久的当前位置。侧栏当前行用中性底 + semibold,右侧页签用文字色 + 底部横条。
|
||||
- Keyboard focus:清晰的强调色焦点环/当前行,不能仅靠 hover 表达。
|
||||
- Pressed:同一色系的短暂加深,不能跳到另一个主题色。
|
||||
- Disabled:保留边界与标签;降低对比,不能像可执行的主按钮。
|
||||
@@ -61,14 +61,21 @@
|
||||
- 左右侧栏的紧凑分组标题统一为 12/16rem、semibold;设置页章节标题仍为 16/16rem。Session 字段名和值同为正文尺寸,以颜色区分层级。
|
||||
- 辅助文字必须在实际的内容、侧栏、浮层背景上均达到 4.5:1;普通路径、数量和说明不再叠加透明度。静态 PID 使用中性文字。
|
||||
- 放大界面字号时,Git 行高与设置导航宽度同步调整;窄侧栏优先保留分支名,长文件名须明确省略并可查看完整路径。
|
||||
- 选中名称用 medium,章节标题用 semibold;正文和辅助说明保持 regular。
|
||||
- 侧栏当前标签页标题用 semibold(测宽与渲染同一字重),章节/分组标题用 semibold;正文和辅助说明保持 regular。
|
||||
- 右侧面板和侧栏的紧凑标题不再大写,靠字重与颜色区分层级。
|
||||
- 以 4px 为间距基础:紧密内容 4–8,行内 8–12,区块 24–32。列表外边距与文字预算同步。
|
||||
- 控件和列表行圆角 6–8px;浮层 12px。除头像/状态点外,避免胶囊泛滥。
|
||||
- 形状(2026-09 v5 风格):单行导航行、侧栏搜索框、工作区切换用胶囊
|
||||
(`rounding::PILL_RADIUS`);图标按钮保持圆角方形(`rounded_lg` / 小号 4px);会长出第二行(分支行)的标签页行保持
|
||||
`CARD_RADIUS`,全圆角会把两行文字的两端挤窄。浮层 `POPOVER_RADIUS` 10px。
|
||||
- 侧栏单行标签页行高 28px,头像 18px 圆形品牌底;搜索框与工作区切换同为 28px。
|
||||
|
||||
## 各界面约束
|
||||
|
||||
- **侧栏**:名称优先于分支和 diff 数量;当前项清晰,品牌头像不形成彩色墙。
|
||||
- **侧栏**:名称优先于分支和 diff 数量;当前项清晰,品牌头像不形成彩色墙。分组标题用正文色
|
||||
semibold,分支与 diff 数量右对齐作次级信息;展开的分组不画 chevron,折叠后才显示。
|
||||
- **侧栏顶部**:导航与内容用留白衔接,不加贯穿的 header 底部分隔线;左右侧栏保持连续表面。
|
||||
- **右侧面板页签**:文字页签(信息 / 更改 / 文件),当前页签正文色 + 底部 2px 横条落在
|
||||
页签栏底部的细分隔线上;「更改」后附改动数。
|
||||
- **右侧文字层级**:文件树名称使用侧栏标题色,不能整体降为辅助灰;当前项使用 selected 文字色与 medium 字重,根目录以字重区分。Git 普通文件名使用 resting 文字色与界面字体,路径、数量与说明保持次级;信息页的路径前缀弱化,末级名称突出。
|
||||
- **文件树**:文件名中性;A/M/? 等尾部字符承载 Git 状态;目录用小点表示子项变化;冲突才整行提升。
|
||||
- **Git 面板**:改动列表是主体。可执行的提交按钮才突出;未填写信息或不可提交时保持次级。
|
||||
|
||||
+1
-1
@@ -10139,7 +10139,7 @@ mod tests {
|
||||
// Both panels pinned to their floors leaves the terminal the rest —
|
||||
// less than its floor, but more than the 260-odd points two saved
|
||||
// widths used to leave it.
|
||||
assert!(narrow - SIDEBAR_MIN - PANEL_MIN > 300.);
|
||||
assert!(narrow - SIDEBAR_MIN - PANEL_MIN >= 300.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+2
-2
@@ -1105,8 +1105,8 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::EditorFileTooLarge => "\"{path}\" is too large for the editor ({size} MB)",
|
||||
L10nKey::EditorBinaryFile => "\"{path}\" looks like a binary file",
|
||||
L10nKey::PanelInfoTitle => "Info",
|
||||
L10nKey::PanelChangesTitle => "Source Control",
|
||||
L10nKey::PanelScmTitle => "Source Control",
|
||||
L10nKey::PanelChangesTitle => "Changes",
|
||||
L10nKey::PanelScmTitle => "Changes",
|
||||
L10nKey::PanelFilesTitle => "Files",
|
||||
L10nKey::PanelNoSession => "No active session.",
|
||||
L10nKey::PanelNoSessionHint => {
|
||||
|
||||
+2
-2
@@ -1170,8 +1170,8 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::EditorFileTooLarge => "「{path}」はエディタで開くには大きすぎます({size} MB)",
|
||||
L10nKey::EditorBinaryFile => "「{path}」はバイナリファイルのようです",
|
||||
L10nKey::PanelInfoTitle => "情報",
|
||||
L10nKey::PanelChangesTitle => "ソース管理",
|
||||
L10nKey::PanelScmTitle => "ソース管理",
|
||||
L10nKey::PanelChangesTitle => "変更",
|
||||
L10nKey::PanelScmTitle => "変更",
|
||||
L10nKey::PanelFilesTitle => "ファイル",
|
||||
L10nKey::PanelNoSession => "アクティブなセッションがありません",
|
||||
L10nKey::PanelNoSessionHint => {
|
||||
|
||||
+2
-2
@@ -1051,8 +1051,8 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::EditorFileTooLarge => "“{path}”太大,无法在编辑器中打开({size} MB)",
|
||||
L10nKey::EditorBinaryFile => "“{path}”看起来是二进制文件",
|
||||
L10nKey::PanelInfoTitle => "信息",
|
||||
L10nKey::PanelChangesTitle => "源代码管理",
|
||||
L10nKey::PanelScmTitle => "源代码管理",
|
||||
L10nKey::PanelChangesTitle => "更改",
|
||||
L10nKey::PanelScmTitle => "更改",
|
||||
L10nKey::PanelFilesTitle => "文件",
|
||||
L10nKey::PanelNoSession => "没有活动会话。",
|
||||
L10nKey::PanelNoSessionHint => "打开一个标签页以在此处查看其 shell、目录和进程。",
|
||||
|
||||
+29
-47
@@ -85,8 +85,6 @@ pub struct Interactions {
|
||||
pub primary: Semantic,
|
||||
pub primary_hover: u32,
|
||||
pub primary_pressed: u32,
|
||||
pub navigation: u32,
|
||||
pub navigation_ink: u32,
|
||||
pub choice: u32,
|
||||
pub choice_ink: u32,
|
||||
pub input_border: u32,
|
||||
@@ -101,16 +99,6 @@ pub mod state {
|
||||
pub const CURSOR: f32 = 1.70;
|
||||
pub const TEXT_RESTING: f32 = 4.6;
|
||||
pub const TEXT_STEP: f32 = 1.4;
|
||||
|
||||
/// The sidebar's selection ladder sits one rung above the window's. The
|
||||
/// window paints a selected row inside a list the user is already looking
|
||||
/// at; the sidebar paints the one tab out of twenty that owns the pane
|
||||
/// area, and at 1.30:1 that tint measured as the faintest mark in the
|
||||
/// column — fainter than a group header's count. `PRESSED` and `CURSOR`
|
||||
/// climb with it so the ladder keeps its spacing.
|
||||
pub const SIDEBAR_SELECTED: f32 = 1.50;
|
||||
pub const SIDEBAR_PRESSED: f32 = 1.75;
|
||||
pub const SIDEBAR_CURSOR: f32 = 1.92;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -119,6 +107,9 @@ pub struct Surface {
|
||||
pub hover: u32,
|
||||
pub selected: u32,
|
||||
pub pressed: u32,
|
||||
/// The top rung. No surface paints it today; the ladder tests hold the
|
||||
/// rungs below it apart against it.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
pub cursor: u32,
|
||||
pub text_resting: u32,
|
||||
pub text_selected: u32,
|
||||
@@ -177,7 +168,10 @@ impl Theme {
|
||||
pub fn neutrals(&self) -> Neutrals {
|
||||
let bg = self.background_color();
|
||||
let fg = legible_foreground(bg, self.foreground);
|
||||
let sidebar = mix(bg, fg, 0.03);
|
||||
// One continuous surface: the rails share the window's fill and are
|
||||
// told apart from the pane area by a hairline alone. A tinted rail
|
||||
// made the workspace read as three boxes set side by side.
|
||||
let sidebar = bg;
|
||||
// Light overlays sit above the grey rail; dark overlays lift toward
|
||||
// the foreground. A light menu must not be darker than its backdrop.
|
||||
let popover = if self.dark { mix(bg, fg, 0.06) } else { bg };
|
||||
@@ -267,15 +261,8 @@ impl Theme {
|
||||
}
|
||||
}
|
||||
|
||||
/// One selection treatment for workspace, settings and panel navigation.
|
||||
pub(crate) fn navigation_colors(&self) -> (u32, u32) {
|
||||
let colors = self.interactions();
|
||||
(colors.navigation, colors.navigation_ink)
|
||||
}
|
||||
|
||||
pub(crate) fn interactions(&self) -> Interactions {
|
||||
let m = self.neutrals();
|
||||
let navigation = mix(m.sidebar, 0x2878df, if self.dark { 0.28 } else { 0.14 });
|
||||
// Menus use a blue wash; the switcher uses the solid companion fill.
|
||||
// Keep labels and shortcut hints readable on the shared menu surface.
|
||||
let choice = mix(m.popover, 0x2878df, if self.dark { 0.34 } else { 0.20 });
|
||||
@@ -296,8 +283,6 @@ impl Theme {
|
||||
},
|
||||
primary_hover: mix(fill, away, 0.08),
|
||||
primary_pressed: mix(fill, away, 0.16),
|
||||
navigation,
|
||||
navigation_ink: legible_foreground(navigation, m.foreground),
|
||||
choice,
|
||||
choice_ink: legible_foreground(choice, m.foreground),
|
||||
input_border: m.border,
|
||||
@@ -392,10 +377,12 @@ impl Theme {
|
||||
pub fn surfaces(&self) -> Surfaces {
|
||||
let m = self.neutrals();
|
||||
let fg = legible_foreground(self.background_color(), self.foreground);
|
||||
// The rail climbs the window's ladder. It used to take a rung above
|
||||
// (1.50) because a regular-weight current tab measured as the faintest
|
||||
// mark in its column; the current title is SEMIBOLD now and carries
|
||||
// that on its own, and at 1.50 on a white rail the row read as a
|
||||
// pressed button rather than as a place.
|
||||
let mut sidebar = self.surface(m.sidebar);
|
||||
sidebar.selected = raise(sidebar.base, fg, state::SIDEBAR_SELECTED);
|
||||
sidebar.pressed = raise(sidebar.base, fg, state::SIDEBAR_PRESSED);
|
||||
sidebar.cursor = raise(sidebar.base, fg, state::SIDEBAR_CURSOR);
|
||||
sidebar.text_resting = m.sidebar_fg;
|
||||
sidebar.text_selected =
|
||||
stepped_ink(sidebar.selected, sidebar.base, fg, sidebar.text_resting);
|
||||
@@ -1214,9 +1201,9 @@ static BUILTINS: [BuiltinSpec; 13] = [
|
||||
id: "light",
|
||||
name: "Light",
|
||||
background: 0xffffff,
|
||||
foreground: 0x111111,
|
||||
accent: 0x007aff,
|
||||
caret: Some(0xf5a15c),
|
||||
foreground: 0x0f1419,
|
||||
accent: 0x1f6bf0,
|
||||
caret: None,
|
||||
ansi16: [
|
||||
(0x24, 0x29, 0x2e),
|
||||
(0xd1, 0x24, 0x2f),
|
||||
@@ -1557,15 +1544,15 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn navigation_labels_remain_legible_in_every_builtin_theme() {
|
||||
fn the_current_tab_row_stays_legible_in_every_builtin_theme() {
|
||||
for theme in builtins() {
|
||||
let (fill, ink) = theme.navigation_colors();
|
||||
let s = theme.surfaces().sidebar;
|
||||
assert!(
|
||||
contrast(fill, ink) >= 4.5,
|
||||
"{} navigation contrast",
|
||||
contrast(s.selected, s.text_selected) >= 4.5,
|
||||
"{} current tab contrast",
|
||||
theme.id
|
||||
);
|
||||
assert_ne!(fill, theme.neutrals().sidebar, "{} selection", theme.id);
|
||||
assert_ne!(s.selected, s.base, "{} selection", theme.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1794,16 +1781,16 @@ mod tests {
|
||||
let bg = dracula.background_color();
|
||||
let s = dracula.surfaces();
|
||||
// The resting rung is checked as `state::SELECTED` on the sidebar
|
||||
// fill — the surface it was signed off on — rather than as the rail's
|
||||
// own fill: the rail was lifted off this value on purpose
|
||||
// (`state::SIDEBAR_SELECTED`) once the tab that owns the pane area
|
||||
// measured as the faintest mark in its own column, and this pin is
|
||||
// here to catch the constant drifting, not that decision.
|
||||
// fill it was signed off on — the old rail, 3% toward the foreground —
|
||||
// rather than on today's rail, which has since gone flat onto the
|
||||
// window fill. This pin is here to catch the constant drifting, not
|
||||
// that decision.
|
||||
let fg = legible_foreground(bg, dracula.foreground);
|
||||
let signed_off_rail = mix(bg, fg, 0.03);
|
||||
for (what, now, legacy) in [
|
||||
(
|
||||
"resting",
|
||||
raise(s.sidebar.base, fg, state::SELECTED),
|
||||
raise(signed_off_rail, fg, state::SELECTED),
|
||||
mix(bg, dracula.foreground, 0.12),
|
||||
),
|
||||
("cursor", s.window.cursor, mix(bg, dracula.foreground, 0.17)),
|
||||
@@ -2113,11 +2100,6 @@ mod tests {
|
||||
"{} choice",
|
||||
theme.id
|
||||
);
|
||||
assert!(
|
||||
contrast(c.navigation, c.navigation_ink) >= 4.5,
|
||||
"{} navigation",
|
||||
theme.id
|
||||
);
|
||||
assert_ne!(
|
||||
c.input_border, c.choice,
|
||||
"{} field boundaries must not use selected-row fills",
|
||||
@@ -2215,10 +2197,10 @@ mod tests {
|
||||
m.caret
|
||||
);
|
||||
}
|
||||
// The default theme is the one that used to fail: an orange caret on
|
||||
// pure white read at 2.07:1.
|
||||
// The default theme used to ship an orange caret that read at 2.07:1
|
||||
// on pure white. It now takes the accent, like the focus ring.
|
||||
let light = builtins().into_iter().find(|t| t.id == DEFAULT_ID).unwrap();
|
||||
assert_ne!(light.neutrals().caret, light.caret.unwrap());
|
||||
assert_eq!(light.neutrals().caret, light.neutrals().accent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+100
-41
@@ -16,7 +16,26 @@ use crate::ui::app::{
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt};
|
||||
use crate::ui::scrollbar::with_vertical_scrollbar;
|
||||
|
||||
pub(crate) const MIN_WIDTH: f32 = 216.;
|
||||
/// Wide enough for the three word tabs, bare, beside the two chrome tiles at
|
||||
/// the default interface size — the Changes count is dropped before a label is
|
||||
/// ever cut. A larger UI font raises the floor past this; see
|
||||
/// [`right_panel_tabs_floor`].
|
||||
pub(crate) const MIN_WIDTH: f32 = 240.;
|
||||
|
||||
/// What the tab row needs with the count dropped: the labels, the row's lead,
|
||||
/// its 2px gaps, and the chrome tiles at the trailing end.
|
||||
fn right_panel_tabs_floor(window: &Window, cx: &gpui::App) -> f32 {
|
||||
let chrome = match cfg!(target_os = "macos") {
|
||||
true => crate::ui::tab_strip::trailing_chrome_tiles_w() + TAB_ROW_GAPS,
|
||||
false => tile_trailing_inset(),
|
||||
};
|
||||
(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.;
|
||||
const TAB_ROW_GAPS: f32 = 4. * 2.;
|
||||
|
||||
/// How wide a panel edge is to grab. Both edges a window can drag — the tab
|
||||
/// sidebar's and this panel's — are the same target, so they are one number.
|
||||
@@ -385,18 +404,26 @@ impl Tty7App {
|
||||
}
|
||||
}
|
||||
|
||||
/// The narrowest the panel may be drawn: `MIN_WIDTH`, or wider when the
|
||||
/// interface font or the language makes the three tab labels and the two
|
||||
/// chrome tiles beside them need more than that.
|
||||
pub(crate) fn right_panel_min_px(&self, window: &Window, cx: &gpui::App) -> f32 {
|
||||
MIN_WIDTH.max(right_panel_tabs_floor(window, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn right_panel_max_px(&self, window: &Window, cx: &gpui::App) -> f32 {
|
||||
crate::ui::app::side_panel_max(
|
||||
window.viewport_size().width.as_f32(),
|
||||
MIN_WIDTH,
|
||||
self.right_panel_min_px(window, cx),
|
||||
self.sidebar_floor(cx) + self.document_floor(cx),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn right_panel_px(&self, window: &Window, cx: &gpui::App) -> f32 {
|
||||
self.right_panel_width
|
||||
.get()
|
||||
.clamp(MIN_WIDTH, self.right_panel_max_px(window, cx))
|
||||
self.right_panel_width.get().clamp(
|
||||
self.right_panel_min_px(window, cx),
|
||||
self.right_panel_max_px(window, cx),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_right_panel(&mut self, cx: &mut Context<Self>) {
|
||||
@@ -439,7 +466,7 @@ impl Tty7App {
|
||||
RightPanelTab::Scm => self.render_panel_scm(window, cx),
|
||||
RightPanelTab::Files => self.render_panel_files(window, cx),
|
||||
};
|
||||
let (backing, handle) = self.right_panel_resize(cx);
|
||||
let (backing, handle) = self.right_panel_resize(window, cx);
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
@@ -466,16 +493,25 @@ impl Tty7App {
|
||||
.on_double_click(|_, window, _| window.titlebar_double_click())
|
||||
.items_center()
|
||||
.gap(px(2.))
|
||||
.pl(px(tile_trailing_inset()))
|
||||
.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(cx))
|
||||
.children(self.right_panel_tabs(
|
||||
width
|
||||
- TAB_ROW_LEAD
|
||||
- crate::ui::tab_strip::trailing_chrome_tiles_w()
|
||||
- TAB_ROW_GAPS,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.child(div().flex_1())
|
||||
// Navigation controls stay visible on both sidebars.
|
||||
.child(self.window_chrome(window, cx))
|
||||
}))
|
||||
// Separate navigation from content with space, matching the
|
||||
// left sidebar's continuous surface.
|
||||
.children(cfg!(target_os = "macos").then(|| div().flex_none().h(px(8.))))
|
||||
.children(cfg!(target_os = "macos").then(|| div().flex_none().h(px(10.))))
|
||||
.child(body)
|
||||
.children(self.sftp_transfers_footer(cx))
|
||||
.child(handle)
|
||||
@@ -483,7 +519,11 @@ impl Tty7App {
|
||||
)
|
||||
}
|
||||
|
||||
fn right_panel_resize(&self, cx: &mut Context<Self>) -> (AnyElement, AnyElement) {
|
||||
fn right_panel_resize(
|
||||
&self,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (AnyElement, AnyElement) {
|
||||
use gpui::{Bounds, MouseButton, MouseMoveEvent, MouseUpEvent, Pixels, canvas};
|
||||
use std::cell::Cell as StdCell;
|
||||
use std::rc::Rc;
|
||||
@@ -494,6 +534,7 @@ impl Tty7App {
|
||||
// be the same one the layout applies or the panel springs back from
|
||||
// wherever it was dropped.
|
||||
let others_floor = self.sidebar_floor(cx) + self.document_floor(cx);
|
||||
let own_floor = self.right_panel_min_px(window, cx);
|
||||
let backing = canvas(
|
||||
{
|
||||
let container = container.clone();
|
||||
@@ -519,10 +560,10 @@ impl Tty7App {
|
||||
let raw = (right - ev.position.x).as_f32();
|
||||
let max = crate::ui::app::side_panel_max(
|
||||
window.viewport_size().width.as_f32(),
|
||||
MIN_WIDTH,
|
||||
own_floor,
|
||||
others_floor,
|
||||
);
|
||||
width_cell.set(raw.clamp(MIN_WIDTH, max));
|
||||
width_cell.set(raw.clamp(own_floor, max));
|
||||
window.refresh();
|
||||
}
|
||||
});
|
||||
@@ -590,8 +631,16 @@ impl Tty7App {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let tabs = (!cfg!(target_os = "macos")).then(|| self.right_panel_tabs(cx));
|
||||
let has_trailing = trailing.is_some();
|
||||
let tabs = (!cfg!(target_os = "macos")).then(|| {
|
||||
let width = self.right_panel_px(window, cx);
|
||||
let trailing_w = match has_trailing {
|
||||
true => 2. * TILE_SIZE_SM + 6.,
|
||||
false => 0.,
|
||||
};
|
||||
let avail = width - TAB_ROW_LEAD - tile_trailing_inset() - trailing_w;
|
||||
self.right_panel_tabs(avail, window, cx)
|
||||
});
|
||||
if tabs.is_none() && !has_trailing {
|
||||
return div().flex_none().into_any_element();
|
||||
}
|
||||
@@ -614,33 +663,43 @@ impl Tty7App {
|
||||
(None, true) => tile_trailing_inset_sm(),
|
||||
(None, false) => CONTENT_INSET,
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.items_baseline()
|
||||
.gap(px(7.))
|
||||
.child(
|
||||
// The title step of the panel ramp, SEMIBOLD and
|
||||
// uppercased. It reads as a label rather than as
|
||||
// content because of the weight and the caps.
|
||||
div()
|
||||
.text_size(rems(META))
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
.child(text.to_uppercase()),
|
||||
)
|
||||
.when_some(count, |this, c| {
|
||||
this.child(
|
||||
// A count is a token hanging off the heading, not
|
||||
// part of it: one step down, mono, regular weight.
|
||||
// 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)
|
||||
})
|
||||
.when(tabs.is_none(), |row| {
|
||||
row.child(
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.items_baseline()
|
||||
.gap(px(7.))
|
||||
.child(
|
||||
// The title step of the panel ramp, SEMIBOLD and
|
||||
// uppercased. It reads as a label rather than as
|
||||
// content because of the weight and the caps.
|
||||
div()
|
||||
.text_size(rems(META_MONO))
|
||||
.font_family(cx.theme().mono_font_family.clone())
|
||||
.text_size(rems(META))
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(c),
|
||||
.child(text.to_string()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.when_some(count, |this, c| {
|
||||
this.child(
|
||||
// A count is a token hanging off the heading, not
|
||||
// part of it: one step down, mono, regular weight.
|
||||
div()
|
||||
.text_size(rems(META_MONO))
|
||||
.font_family(cx.theme().mono_font_family.clone())
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(c),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_w_0())
|
||||
.when_some(trailing, |this, t| this.child(t))
|
||||
.when_some(tabs, |this, tiles| {
|
||||
@@ -1113,12 +1172,12 @@ impl Tty7App {
|
||||
}))
|
||||
.pb(px(if trailing.is_some() { 0. } else { 4. }))
|
||||
.child(
|
||||
// Weight and capitalization distinguish compact group headings.
|
||||
// Weight and ink, not capitals, set a compact heading apart.
|
||||
div()
|
||||
.text_size(rems(HEADING))
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(text.to_uppercase()),
|
||||
.child(text.to_string()),
|
||||
)
|
||||
.when_some(trailing, |this, t| this.child(t))
|
||||
.into_any_element()
|
||||
|
||||
+5
-1
@@ -16,7 +16,11 @@ pub(crate) const TRACK_RADIUS: Pixels = px(8.);
|
||||
/// Inset cards, interactive rows, and floating panels share the A style scale.
|
||||
pub(crate) const CARD_RADIUS: Pixels = px(10.);
|
||||
pub(crate) const ROW_RADIUS: Pixels = px(7.);
|
||||
pub(crate) const POPOVER_RADIUS: Pixels = px(13.);
|
||||
pub(crate) const POPOVER_RADIUS: Pixels = px(10.);
|
||||
|
||||
/// Single-line navigation rows, search fields and round icon buttons. Larger
|
||||
/// than any row is tall, so it always resolves to a full half-height cap.
|
||||
pub(crate) const PILL_RADIUS: Pixels = px(999.);
|
||||
|
||||
pub(crate) const HAIRLINE: Pixels = px(1.);
|
||||
|
||||
|
||||
+46
-25
@@ -31,6 +31,10 @@ const GRAB_HANDLE_W: f32 = 48.;
|
||||
|
||||
const ROW_GAP: f32 = 2.;
|
||||
|
||||
/// A single-line tab row: one line of `text_sm` and a little air, the same
|
||||
/// 28px the search field and the workspace chip above it stand at.
|
||||
const ROW_HEIGHT: f32 = 28.;
|
||||
|
||||
/// The row chrome the text budget has to be measured around. These are the
|
||||
/// numbers the layout below is built from, not a second guess at it — a row
|
||||
/// that elides against a budget wider than it really has falls back to CSS
|
||||
@@ -43,7 +47,7 @@ mod row_metrics {
|
||||
/// `pl_2` + `pr_2` on the row.
|
||||
pub(super) const ROW_PAD: f32 = 8.;
|
||||
/// The avatar handed to `tab_avatar`.
|
||||
pub(super) const AVATAR: f32 = 22.;
|
||||
pub(super) const AVATAR: f32 = 18.;
|
||||
/// `gap_2` between the row's children.
|
||||
pub(super) const GAP: f32 = 8.;
|
||||
/// The ⌘N badge, when one is shown.
|
||||
@@ -181,9 +185,9 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
/// What the right panel has reserved, from the sidebar's point of view.
|
||||
pub(crate) fn right_panel_floor(&self, cx: &gpui::App) -> f32 {
|
||||
pub(crate) fn right_panel_floor(&self, window: &Window, cx: &gpui::App) -> f32 {
|
||||
if self.right_panel_open(cx) {
|
||||
crate::ui::right_panel::MIN_WIDTH
|
||||
self.right_panel_min_px(window, cx)
|
||||
} else {
|
||||
0.
|
||||
}
|
||||
@@ -193,7 +197,7 @@ impl Tty7App {
|
||||
crate::ui::app::side_panel_max(
|
||||
window.viewport_size().width.as_f32(),
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
self.right_panel_floor(cx) + self.document_floor(cx),
|
||||
self.right_panel_floor(window, cx) + self.document_floor(cx),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,7 +321,7 @@ impl Tty7App {
|
||||
// it as regular would let the one row the user is looking at overflow
|
||||
// into the truncation this is here to avoid.
|
||||
let title_font_active = gpui::Font {
|
||||
weight: FontWeight::MEDIUM,
|
||||
weight: FontWeight::SEMIBOLD,
|
||||
..font.clone()
|
||||
};
|
||||
let rem = window.rem_size().as_f32();
|
||||
@@ -703,6 +707,10 @@ impl Tty7App {
|
||||
let success = added_ink;
|
||||
let danger = removed_ink;
|
||||
|
||||
// A row that is one line is a pill; one that grows a branch
|
||||
// line under its title keeps a card's corners, since a full
|
||||
// radius on a two-line box pinches the text at both ends.
|
||||
let two_line = git_line.is_some();
|
||||
let label_region = match rename_input {
|
||||
Some(input) => div()
|
||||
.id(("sidebar-rename", i))
|
||||
@@ -819,7 +827,7 @@ impl Tty7App {
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_sm()
|
||||
.when(is_active, |d| d.font_weight(FontWeight::MEDIUM))
|
||||
.when(is_active, |d| d.font_weight(FontWeight::SEMIBOLD))
|
||||
.child(shown_title),
|
||||
)
|
||||
// The path is elided to the room the title
|
||||
@@ -866,13 +874,17 @@ impl Tty7App {
|
||||
}
|
||||
})
|
||||
.w_full()
|
||||
.py_2()
|
||||
.min_h(px(ROW_HEIGHT))
|
||||
.when(two_line, |s| s.py_1p5())
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.pl_2()
|
||||
.pr_2()
|
||||
.rounded(crate::ui::rounding::CARD_RADIUS)
|
||||
.rounded(match two_line {
|
||||
true => crate::ui::rounding::CARD_RADIUS,
|
||||
false => crate::ui::rounding::PILL_RADIUS,
|
||||
})
|
||||
.when(is_active, |s| {
|
||||
s.bg(cx.theme().sidebar_accent)
|
||||
.text_color(cx.theme().sidebar_accent_foreground)
|
||||
@@ -921,7 +933,7 @@ impl Tty7App {
|
||||
agent_status,
|
||||
agent_unread,
|
||||
ssh_dot,
|
||||
22.,
|
||||
row_metrics::AVATAR,
|
||||
cx,
|
||||
))
|
||||
// Leading, like the chip's: the trailing end of a row is
|
||||
@@ -959,7 +971,10 @@ impl Tty7App {
|
||||
row.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top(px(4.))
|
||||
.top(px(match two_line {
|
||||
true => 4.,
|
||||
false => (ROW_HEIGHT - crate::ui::tab_strip::MIN_TARGET) / 2.,
|
||||
}))
|
||||
.right(px(6.))
|
||||
.opacity(0.)
|
||||
.group_hover(SharedString::from(format!("tab-row-{i}")), |s| {
|
||||
@@ -973,7 +988,7 @@ impl Tty7App {
|
||||
),
|
||||
))
|
||||
.child(
|
||||
div().bg(backing).child(
|
||||
div().bg(backing).rounded_full().child(
|
||||
crate::ui::tab_strip::hit_target(
|
||||
Button::new(("sidebar-close", i))
|
||||
.icon(IconName::Close)
|
||||
@@ -1142,15 +1157,16 @@ impl Tty7App {
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div().flex_shrink_0().child(
|
||||
Icon::new(match folded {
|
||||
true => IconName::ChevronRight,
|
||||
false => IconName::ChevronDown,
|
||||
})
|
||||
.xsmall(),
|
||||
),
|
||||
)
|
||||
// The heading names the group; the chevron only says
|
||||
// something when there is something behind it. An open
|
||||
// group shows its rows, which is its own answer.
|
||||
.when(folded, |header| {
|
||||
header.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(Icon::new(IconName::ChevronRight).xsmall()),
|
||||
)
|
||||
})
|
||||
.when(pinned, |header| {
|
||||
header.child(
|
||||
div()
|
||||
@@ -1179,6 +1195,10 @@ impl Tty7App {
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
// Body ink, not caption grey: the heading is the
|
||||
// name of what sits under it, and the branch and
|
||||
// counts beside it are the metadata.
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(label)
|
||||
.into_any_element(),
|
||||
})
|
||||
@@ -1447,12 +1467,13 @@ impl Tty7App {
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.gap(px(6.))
|
||||
.h(px(34.))
|
||||
.h(px(ROW_HEIGHT))
|
||||
.mx_2()
|
||||
.mt_1()
|
||||
.mt_1p5()
|
||||
.mb_1()
|
||||
.px_2()
|
||||
.rounded_lg()
|
||||
.pl(px(6.))
|
||||
.pr_1()
|
||||
.rounded(crate::ui::rounding::PILL_RADIUS)
|
||||
.bg(cx.theme().muted)
|
||||
.child(
|
||||
div()
|
||||
@@ -1481,7 +1502,7 @@ impl Tty7App {
|
||||
// below only ever sees a `Window`, and the cap it clamps against has to
|
||||
// be the same one the layout applies or the sidebar springs back from
|
||||
// wherever it was dropped.
|
||||
let others_floor = self.right_panel_floor(cx) + self.document_floor(cx);
|
||||
let others_floor = self.right_panel_floor(window, cx) + self.document_floor(cx);
|
||||
let backing = canvas(
|
||||
{
|
||||
let container = container.clone();
|
||||
|
||||
+156
-90
@@ -600,20 +600,47 @@ pub(crate) fn chrome_tile(button: Button, selected: bool, cx: &gpui::App) -> But
|
||||
chrome_tile_sized(button, TILE_SIZE, TILE_GLYPH, selected, cx)
|
||||
}
|
||||
|
||||
/// Secondary panel navigation stays neutral; workspace selection owns accent.
|
||||
pub(crate) fn chrome_tile_marked(button: Button, current: bool, cx: &gpui::App) -> Button {
|
||||
button
|
||||
.custom(chrome_tile_variant_for(current, cx))
|
||||
.when(current, |button| {
|
||||
button
|
||||
.bg(cx.theme().secondary)
|
||||
.text_color(cx.theme().foreground)
|
||||
})
|
||||
.with_size(px(TILE_GLYPH / BUTTON_ICON_SCALE))
|
||||
.w(px(TILE_SIZE))
|
||||
.h(px(TILE_SIZE))
|
||||
const RIGHT_PANEL_TABS: [(RightPanelTab, L10nKey); 3] = [
|
||||
(RightPanelTab::Info, L10nKey::PanelInfoTitle),
|
||||
(RightPanelTab::Scm, L10nKey::PanelChangesTitle),
|
||||
(RightPanelTab::Files, L10nKey::PanelFilesTitle),
|
||||
];
|
||||
|
||||
fn right_panel_tab_size(window: &Window) -> f32 {
|
||||
window.rem_size().as_f32() * crate::ui::right_panel::META
|
||||
}
|
||||
|
||||
fn right_panel_tab_font(cx: &gpui::App) -> gpui::Font {
|
||||
gpui::Font {
|
||||
family: cx.theme().font_family.clone(),
|
||||
features: Default::default(),
|
||||
fallbacks: None,
|
||||
weight: FontWeight::SEMIBOLD,
|
||||
style: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the three bare tab labels take, padding included, at the live
|
||||
/// interface size and language. The panel's floor is built on it, so a larger
|
||||
/// UI font widens the panel instead of pushing its chrome tiles off the edge.
|
||||
pub(crate) fn right_panel_tab_labels_w(window: &Window, cx: &gpui::App) -> f32 {
|
||||
let size = right_panel_tab_size(window);
|
||||
let font = right_panel_tab_font(cx);
|
||||
RIGHT_PANEL_TABS
|
||||
.iter()
|
||||
.map(|(_, key)| {
|
||||
measure_text(window.text_system(), &font, size, t(*key))
|
||||
+ 2. * (TAB_OUTER_PAD + TAB_INNER_PAD)
|
||||
})
|
||||
.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.;
|
||||
const TAB_COUNT_GAP: f32 = 5.;
|
||||
|
||||
/// How wide the two chrome tiles at the trailing end of the title bar are, with
|
||||
/// the padding around them.
|
||||
pub(crate) fn trailing_chrome_tiles_w() -> f32 {
|
||||
@@ -981,10 +1008,10 @@ impl Tty7App {
|
||||
.id("workspace-rename")
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.h(px(30.))
|
||||
.h(px(28.))
|
||||
.w_full()
|
||||
.px(px(7.))
|
||||
.rounded_md()
|
||||
.px(px(9.))
|
||||
.rounded(crate::ui::rounding::PILL_RADIUS)
|
||||
.bg(cx.theme().sidebar_accent)
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(Input::new(&rename.input).appearance(false).xsmall())
|
||||
@@ -1050,8 +1077,9 @@ impl Tty7App {
|
||||
.flex_shrink(1.)
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_size(px(12.5))
|
||||
.text_size(px(13.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().foreground)
|
||||
.child(SharedString::from(current.clone())),
|
||||
)
|
||||
.child(
|
||||
@@ -1065,8 +1093,8 @@ impl Tty7App {
|
||||
)
|
||||
.xsmall()
|
||||
.w_full()
|
||||
.h(px(30.))
|
||||
.rounded_md()
|
||||
.h(px(28.))
|
||||
.rounded(crate::ui::rounding::PILL_RADIUS)
|
||||
.tooltip_element(chord_tooltip(
|
||||
t(L10nKey::HomeSwitchWorkspace),
|
||||
"ToggleSwitcher",
|
||||
@@ -1158,7 +1186,18 @@ impl Tty7App {
|
||||
.child(self.app_menu_tile(window, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn right_panel_tabs(&self, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
||||
/// The right panel's word tabs, laid out in `avail` px.
|
||||
///
|
||||
/// A label is never elided — "C…" is not a tab anyone can read — so when
|
||||
/// the row runs short the Changes count is what gives way: it repeats what
|
||||
/// the panel body says, the label does not. `right_panel::MIN_WIDTH` is
|
||||
/// what guarantees the three bare labels always fit.
|
||||
pub(crate) fn right_panel_tabs(
|
||||
&self,
|
||||
avail: f32,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
let active_tab = self.right_panel_tab;
|
||||
// The count the source control tile carries. It reads the same status
|
||||
// the panel draws, so the badge and the group headers can never
|
||||
@@ -1171,78 +1210,105 @@ 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);
|
||||
[
|
||||
(
|
||||
RightPanelTab::Info,
|
||||
Icon::empty().path("icons/info.svg"),
|
||||
L10nKey::PanelInfoTitle,
|
||||
),
|
||||
(
|
||||
RightPanelTab::Scm,
|
||||
Icon::empty().path("icons/git-branch.svg"),
|
||||
L10nKey::PanelChangesTitle,
|
||||
),
|
||||
(
|
||||
RightPanelTab::Files,
|
||||
Icon::new(IconName::FolderClosed),
|
||||
L10nKey::PanelFilesTitle,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(tab, icon, label_key)| {
|
||||
let current = active_tab == tab;
|
||||
let tile = chrome_tile_marked(
|
||||
Button::new(("right-panel-tab", tab as usize)).icon(icon),
|
||||
current,
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip(match (tab, changed) {
|
||||
(RightPanelTab::Scm, Some(n)) => {
|
||||
SharedString::from(format!("{} · {n}", t(label_key)))
|
||||
}
|
||||
_ => SharedString::from(t(label_key)),
|
||||
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();
|
||||
let labels_w = right_panel_tab_labels_w(window, cx);
|
||||
let changed = changed.filter(|n| {
|
||||
let regular = gpui::Font {
|
||||
weight: FontWeight::NORMAL,
|
||||
..font.clone()
|
||||
};
|
||||
labels_w + TAB_COUNT_GAP + measure_text(ts, ®ular, size, &n.to_string()) <= avail
|
||||
});
|
||||
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.
|
||||
let ink = match current {
|
||||
true => cx.theme().foreground,
|
||||
false => cx.theme().muted_foreground,
|
||||
};
|
||||
let count = match tab {
|
||||
RightPanelTab::Scm => changed,
|
||||
_ => None,
|
||||
};
|
||||
div()
|
||||
.id(("right-panel-tab", tab as usize))
|
||||
// The press must not start a window drag from the title bar
|
||||
// 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))
|
||||
.cursor_pointer()
|
||||
.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_color(ink)
|
||||
.child(div().flex_shrink_0().child(t(label_key)))
|
||||
.when_some(count, |row, n| {
|
||||
row.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.font_weight(FontWeight::NORMAL)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(n.to_string()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.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
|
||||
// `ToggleRightPanel` and the chrome tile beside them are still
|
||||
// what brings it back.)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
match this.right_panel_open(cx) && this.right_panel_tab == tab {
|
||||
true => {
|
||||
this.toggle_right_panel(cx);
|
||||
// These tabs live inside the panel, so closing
|
||||
// from one destroys the element that holds the
|
||||
// focus and leaves it nowhere — and a keymap whose
|
||||
// bindings are scoped to a focused thing goes
|
||||
// quiet with it. Hand the terminal back what it
|
||||
// lost.
|
||||
this.focus_active(window, cx);
|
||||
}
|
||||
false => this.set_right_panel_tab(tab, cx),
|
||||
}
|
||||
}))
|
||||
.into_any_element()
|
||||
})
|
||||
// A tile for another tab switches to it; the lit one puts
|
||||
// the panel away, the way an activity bar behaves
|
||||
// everywhere else. Pressing it used to do nothing at all
|
||||
// — a dead click on the one control in the row that looks
|
||||
// like it should undo itself. (These tiles only exist
|
||||
// while the panel is open, so `ToggleRightPanel` and the
|
||||
// chrome tile beside them are still what brings it back.)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
match this.right_panel_open(cx) && this.right_panel_tab == tab {
|
||||
true => {
|
||||
this.toggle_right_panel(cx);
|
||||
// These tiles live inside the panel, so
|
||||
// closing from one destroys the element that
|
||||
// holds the focus and leaves it nowhere —
|
||||
// and a keymap whose bindings are scoped to a
|
||||
// focused thing goes quiet with it, so the
|
||||
// ⌘J that would undo this did nothing at all.
|
||||
// Hand the terminal back what it lost.
|
||||
this.focus_active(window, cx);
|
||||
}
|
||||
false => this.set_right_panel_tab(tab, cx),
|
||||
}
|
||||
}));
|
||||
div()
|
||||
.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 glyph.
|
||||
// The `occlude` that keeps a press from dragging the window
|
||||
// stays on the tile: grown to the whole row it would take the
|
||||
// few pixels above and below each glyph out of the drag
|
||||
// region and hand them to nothing.
|
||||
.h_full()
|
||||
.relative()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(div().occlude().flex_shrink_0().child(tile))
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Working and Done differ only in hue (blue vs green), and Waiting vs Done
|
||||
|
||||
+5
-3
@@ -832,8 +832,10 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
|
||||
t.shadow = false;
|
||||
|
||||
let sidebar_bg = Hsla::from(rgb(m.sidebar));
|
||||
let (navigation_fill, navigation_ink) = theme.navigation_colors();
|
||||
let sidebar_sel = rgb(navigation_fill);
|
||||
// The current tab is a place, not an action: a neutral step of the rail's
|
||||
// own ladder plus a heavier title, never the accent. Blue is left to the
|
||||
// things that can be pressed.
|
||||
let sidebar_sel = rgb(surfaces.sidebar.selected);
|
||||
// `t.sidebar` stays the opaque theme token: the settings theme picker
|
||||
// paints with it on top of the (opaque) settings overlay, so diluting
|
||||
// it would wash out that panel. The workspace sidebar/right-panel
|
||||
@@ -847,7 +849,7 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
|
||||
t.sidebar_foreground = rgb(surfaces.sidebar.text_resting).into();
|
||||
t.sidebar_accent = sidebar_sel.into();
|
||||
t.tokens.sidebar_accent = Hsla::from(sidebar_sel).into();
|
||||
t.sidebar_accent_foreground = rgb(navigation_ink).into();
|
||||
t.sidebar_accent_foreground = rgb(surfaces.sidebar.text_selected).into();
|
||||
|
||||
t.list.active_highlight = true;
|
||||
t.list_active = rgb(interaction.choice).into();
|
||||
|
||||
Reference in New Issue
Block a user