diff --git a/CLAUDE.md b/CLAUDE.md index 1cb9f93..dff9d38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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)的配色。不做自定义主题、导入、按主机指定。 - **键盘快捷键**:设置左栏单独一个分类,排在「关键字高亮」下面,分通用、标签、终端、编辑器四组。 diff --git a/src/app/mod.rs b/src/app/mod.rs index 393eafd..a41ae16 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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); + } + } + } + }); + } +} diff --git a/src/credential/credential_panel.rs b/src/credential/credential_panel.rs index bd2e83f..01dec17 100644 --- a/src/credential/credential_panel.rs +++ b/src/credential/credential_panel.rs @@ -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() diff --git a/src/forward/forward_panel.rs b/src/forward/forward_panel.rs index 5a89204..7547a11 100644 --- a/src/forward/forward_panel.rs +++ b/src/forward/forward_panel.rs @@ -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() diff --git a/src/host/host_panel.rs b/src/host/host_panel.rs index 6b8ccaa..57d3811 100644 --- a/src/host/host_panel.rs +++ b/src/host/host_panel.rs @@ -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())) diff --git a/src/workspace/recent_hosts.rs b/src/workspace/recent_hosts.rs index c5cd8d9..c0a37c5 100644 --- a/src/workspace/recent_hosts.rs +++ b/src/workspace/recent_hosts.rs @@ -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() diff --git a/tests/ui/settings.rs b/tests/ui/settings.rs index 677514c..f7a6626 100644 --- a/tests/ui/settings.rs +++ b/tests/ui/settings.rs @@ -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);