列表的悬停和选中分开:深色悬停调淡,选中行统一一种颜色

- 侧栏的主机、端口转发、凭据列表和「最近连接」的选中行同色:浅色是原来侧栏的深一档灰,深色用「最近连接」原来的选中色,不再刺眼
- 深色下列表和表格的悬停只有选中色的一半,和选中区分开
- 浅色「最近连接」的选中行原来和悬停一样,现在同侧栏

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
XuzandClaude Opus 5.5 committed 2026-10-07 20:37:48 +08:00
1 parent 3ddc7ebe62
commit f8f7473e42
7 files changed
+87 -15

No files matched your search

+1 -1
View File
@@ -69,7 +69,7 @@ ShellRS(crate 与二进制都叫 `shellrs`)是 Xshell / WinSCP 式的 SSH
- **设置 › 应用 › 窗口**:「记住窗口大小」(含最大化)和「记住窗口位置」两个开关,默认都开,下次启动生效。屏幕不在了或窗口不在屏幕上就居中,比屏幕大就缩小;全屏不记。
- **终端通知**:只做程序请求的(OSC 9 / 777)和响铃,不做 OSC 133 长命令通知:那要每台服务器装 shell 集成,收益不抵成本。不在前台发系统通知,在前台而终端在别的标签发应用内通知;响铃在终端就在眼前时不提醒。
- **主题**:「应用外观」(浅色 / 深色 / 跟随系统)决定深浅;「外观 › 主题」左右两栏里浅色、深色各选一套,按当前外观生效。组里第一项「界面跟随主题」默认关闭:界面是中性的灰(浅色是 gpui-kit 默认主题,深色见下条),只有终端按主题配色;打开后界面也按主题配色。用户试过默认跟随,觉得中性界面更合适,别改回去。
- 默认深色(ShellRS Dark)不用 gpui-kit 的近黑,用参照微信深色的炭灰:底色 `#1e1e1f`,标题栏、标签栏、状态栏 `#2f2f30`,边框和悬停 `#3b3b3d`,文字 `#e0e0e4`。用户嫌近黑刺眼。次要文字比微信的 `#7b7b7f` 亮一些(`#929296`),保持对比度 ≥ 4.5。页面「重置」对卡片和开关各管各的。
- 默认深色(ShellRS Dark)不用 gpui-kit 的近黑,用参照微信深色的炭灰:底色 `#1e1e1f`,标题栏、标签栏、状态栏 `#2f2f30`,边框和悬停 `#3b3b3d`,文字 `#e0e0e4`。用户嫌近黑刺眼。次要文字比微信的 `#7b7b7f` 亮一些(`#929296`),保持对比度 ≥ 4.5。侧栏三个列表和「最近连接」的选中行同色(`selected_row_color`):浅色是悬停色三倍的对比,深色那样太刺眼,用加深后的悬停色,列表和表格的悬停只有它的一半(`DARK_HOVER_SHARE`)。两种外观下悬停和选中都要分得开。页面「重置」对卡片和开关各管各的。
- 左栏只列浅色主题,右栏只列深色主题;卡片是名称加示例输出,选中的整张加底色,右上角是蓝底(主题的 `blue`)对勾。
- 只有内置的,浅色、深色各 10 套,默认 ShellRS Light / Dark(底色与界面一致),Solarized 第二,Tokyo Day / Tokyo Night 第三。同一主题的浅、深两版在两栏同一行,没有另一版的(Rosé Pine Dawn、Everforest Light、Dracula、Nord)排最后。只收宽松许可(MIT、Apache-2.0)的配色。不做自定义主题、导入、按主机指定。
- **键盘快捷键**:设置左栏单独一个分类,排在「关键字高亮」下面,分通用、标签、终端、编辑器四组。
+66 -7
View File
@@ -52,23 +52,82 @@ pub fn init(cx: &mut App) {
cx.on_action(|_: &Quit, cx: &mut App| quit::quit(cx));
}
/// How much of a selected row's color hover gets in the dark appearance.
/// Selected rows there have the deepened hover color itself; hover with all
/// of it would look the same.
const DARK_HOVER_SHARE: f32 = 0.5;
/// Use a stronger version of the theme's neutral list hover color throughout
/// the app.
/// the app. Dark, the stronger one is for selected rows
/// (`selected_row_color`) and hover, in lists and tables, gets half of it.
pub(crate) fn deepen_list_hover(cx: &mut App) {
let theme = Theme::global_mut(cx);
let hover = theme.list_hover;
let deeper = hover.alpha(((hover.a + 0.2) * 1.2).min(1.0));
theme.list_hover = deeper;
theme.tokens.list_hover = deeper.into();
if theme.is_dark() {
let faint = deeper.alpha(deeper.a * DARK_HOVER_SHARE);
theme.list_hover = faint;
theme.tokens.list_hover = faint.into();
theme.table_hover = faint;
theme.tokens.table_hover = faint.into();
} else {
theme.list_hover = deeper;
theme.tokens.list_hover = deeper.into();
}
Theme::sync_base(cx);
}
/// Give the host tree three times the list hover's contrast against its sidebar.
/// The recent-host list keeps the shared hover color unchanged.
pub(crate) fn host_tree_selection_color(theme: &Theme) -> Hsla {
/// The deepened list hover: light the hover itself, dark the full color
/// hover has only a share of.
fn deepened_hover(theme: &Theme) -> Hsla {
let hover = theme.list_hover;
if theme.is_dark() {
hover.alpha((hover.a / DARK_HOVER_SHARE).min(1.0))
} else {
hover
}
}
/// The selected row of the sidebar's lists (hosts, port forwards,
/// credentials) and of the recent hosts, so that it stands out from hover.
/// Light, three times the list hover's contrast against the sidebar; dark,
/// where that glares, the deepened hover.
pub(crate) fn selected_row_color(theme: &Theme) -> Hsla {
let sidebar = theme.sidebar;
let hover = sidebar.blend(theme.list_hover);
let hover = sidebar.blend(deepened_hover(theme));
let mut selected = hover.alpha(1.0);
if theme.is_dark() {
return selected;
}
selected.l = (sidebar.l + (hover.l - sidebar.l) * 3.0).clamp(0.0, 1.0);
selected
}
#[cfg(test)]
mod tests {
use gpui_kit::TestAppContext;
use gpui_kit::component::{ActiveTheme as _, Theme, ThemeMode};
use super::{deepen_list_hover, deepened_hover};
#[gpui_kit::test]
fn dark_hover_is_fainter_than_a_selected_row(cx: &mut TestAppContext) {
cx.update(|cx| {
gpui_kit::component::init(cx);
for mode in [ThemeMode::Light, ThemeMode::Dark] {
Theme::change(mode, None, cx);
deepen_list_hover(cx);
let theme = cx.theme();
let selected = deepened_hover(theme);
assert!(selected.a > 0.9, "{mode:?}: {selected:?}");
match mode {
ThemeMode::Light => assert_eq!(theme.list_hover, selected),
ThemeMode::Dark => {
assert!((theme.list_hover.a * 2. - selected.a).abs() < 0.01);
assert_eq!(theme.table_hover, theme.list_hover);
}
}
}
});
}
}
+1 -1
View File
@@ -295,7 +295,7 @@ impl CredentialPanel {
.confirmed(selected)
.aria_selected(selected)
.when(selected, |item| {
item.bg(crate::app::host_tree_selection_color(theme))
item.bg(crate::app::selected_row_color(theme))
})
.child(
h_flex()
+1 -1
View File
@@ -320,7 +320,7 @@ impl ForwardPanel {
.confirmed(selected)
.aria_selected(selected)
.when(selected, |item| {
item.bg(crate::app::host_tree_selection_color(theme))
item.bg(crate::app::selected_row_color(theme))
})
.child(
h_flex()
+1 -1
View File
@@ -608,7 +608,7 @@ fn render_row(
// Make selection stronger than hover against the sidebar background.
.confirmed(selected)
.when(selected, |row| {
row.bg(crate::app::host_tree_selection_color(cx.theme()))
row.bg(crate::app::selected_row_color(cx.theme()))
})
.pl(rems(0.75 + entry.depth() as f32))
.child(h_flex().gap_2().child(mark).child(item.label.clone()))
+3 -1
View File
@@ -190,7 +190,9 @@ impl RecentHosts {
.py_2()
.rounded(cx.theme().radius)
.confirmed(selected)
.when(selected, |item| item.bg(cx.theme().tokens.list_hover))
.when(selected, |item| {
item.bg(crate::app::selected_row_color(cx.theme()))
})
.child(
h_flex()
.gap_3()
+14 -3
View File
@@ -2,13 +2,24 @@
use crate::support::*;
/// The app's list hover is in effect, not the theme's own (a 0.6 alpha):
/// deepened, and dark halved to stay fainter than a selected row.
fn app_list_hover(theme: &gpui_kit::component::Theme) -> bool {
let alpha = theme.list_hover.a;
if theme.is_dark() {
(0.4..0.55).contains(&alpha)
} else {
alpha > 0.9
}
}
#[gpui_kit::test]
fn theme_toggle_flips_mode(cx: &mut TestAppContext) {
let (handle, _) = open_workspace(cx);
let before = cx.update(|cx| {
let theme = cx.theme();
assert_eq!(theme.list_hover, theme.tokens.list_hover.color);
assert!(theme.list_hover.a > 0.9);
assert!(app_list_hover(theme), "{:?}", theme.list_hover);
theme.is_dark()
});
@@ -22,7 +33,7 @@ fn theme_toggle_flips_mode(cx: &mut TestAppContext) {
let after = cx.update(|cx| {
let theme = cx.theme();
assert_eq!(theme.list_hover, theme.tokens.list_hover.color);
assert!(theme.list_hover.a > 0.9);
assert!(app_list_hover(theme), "{:?}", theme.list_hover);
theme.is_dark()
});
assert_ne!(before, after);
@@ -296,7 +307,7 @@ fn the_appearance_setting_drives_the_theme_and_the_title_bar_switch(cx: &mut Tes
assert!(theme.is_dark());
// Applied like the title bar switch always did it.
assert_eq!(theme.list_hover, theme.tokens.list_hover.color);
assert!(theme.list_hover.a > 0.9);
assert!(app_list_hover(theme), "{:?}", theme.list_hover);
});
cx.update_window(handle.into(), |_, window, cx| {
window.render_frame(cx);