From 6c04f0e8c1aadb186471cd48f930b282554041ef Mon Sep 17 00:00:00 2001 From: Bill Date: Wed, 7 Oct 2026 21:24:27 +0800 Subject: [PATCH] feat(theme): add background image dim scrim slider New terminal.backgroundImageDim setting (0-90%, default 0 = off). A black scrim layer sits between the background image and the xterm screen, so bright wallpapers stay readable at any opacity: unlike the opacity slider (which blends the image toward the dark dock base), the scrim darkens the image while preserving its saturation, and the raised minimumContrastRatio (4.5) keeps lifted text legible on top. - settings: backgroundImageDim with deepMerge type-fallback sanitize - TerminalView: render .term-bg-dim only when image set and dim > 0 - ThemeSettingsTab: slider follows the existing draft + onChangeComplete pattern (no settings writes while dragging) - i18n: settings.theme.backgroundImageDim(+Desc) in zh-CN/zh-TW/en/ja - AGENTS.md: document the third image-mode invariant --- AGENTS.md | 2 +- .../src/settings/ThemeSettingsTab.tsx | 21 +++++++++++++++++++ src/renderer/src/terminal/TerminalView.tsx | 8 +++++++ src/renderer/src/terminal/terminal.css | 11 ++++++++++ src/shared/i18n/dicts/en/settings.ts | 2 ++ src/shared/i18n/dicts/ja/settings.ts | 2 ++ src/shared/i18n/dicts/zh-CN/settings.ts | 2 ++ src/shared/i18n/dicts/zh-TW/settings.ts | 2 ++ src/shared/settings.ts | 5 ++++- 9 files changed, 53 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fb83122..bd107e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。 - 终端主题由 xterm 主题派生 UI 配色:`src/renderer/src/theme/chrome.ts` 的 `applyChromeTheme` 写入 `--chrome-bg/-bg-deep/-border/-hover` CSS 变量,antd token 在 `main.tsx` ThemedConfigProvider 派生 - 标签强调色:`settings.tabAccentColor`(默认 `#3fb950`),经 `--tab-accent` CSS 变量生效 -- 背景图(`settings.terminal.backgroundImage`):图片层 `.term-bg-image` 在 xterm 画布之下(`terminal.css`),xterm 背景设 `#00000000` 透明。**图片模式下的两条铁律**:① `.terminal-view.has-bg-image .terminal-view-dock` 强制深色底衬 `#0d1117`,不能用 `--chrome-bg`——浅色主题下它是白色,调低透明度会把图洗白而不是压暗;② `minimumContrastRatio` 从 1 提到 4.5(WCAG AA,与 VS Code 终端默认值一致;TerminalView 构造与设置热更两处都要改;注意正确拼写是 `minimumContrastRatio`,`minContrastRatio` 会被静默忽略),xterm 的对比度计算把透明背景当黑色亮度,会把浅色主题的深色前景自动提亮到可读。无图时保持 1 不动主题配色 +- 背景图(`settings.terminal.backgroundImage`):图片层 `.term-bg-image` 在 xterm 画布之下(`terminal.css`),xterm 背景设 `#00000000` 透明。**图片模式下的三条铁律**:① `.terminal-view.has-bg-image .terminal-view-dock` 强制深色底衬 `#0d1117`,不能用 `--chrome-bg`——浅色主题下它是白色,调低透明度会把图洗白而不是压暗;② `minimumContrastRatio` 从 1 提到 4.5(WCAG AA,与 VS Code 终端默认值一致;TerminalView 构造与设置热更两处都要改;注意正确拼写是 `minimumContrastRatio`,`minContrastRatio` 会被静默忽略),xterm 的对比度计算把透明背景当黑色亮度,会把浅色主题的深色前景自动提亮到可读。无图时保持 1 不动主题配色。③ 压暗层(`settings.terminal.backgroundImageDim`,0–90,**默认 0**):`.term-bg-dim` 纯黑 scrim 在 DOM 序上位于 `.term-bg-image` 之后、xterm(z-index:1)之下(同在 z-index:0 层,靠 DOM 序压图),与透明度的区别是透明度把图与深色底衬混合、压暗在图之上叠黑(保饱和但整体变暗);**dim=0 时 TerminalView 不渲染该层,零成本** ## 终端尺寸同步 diff --git a/src/renderer/src/settings/ThemeSettingsTab.tsx b/src/renderer/src/settings/ThemeSettingsTab.tsx index 92d47d7..ef1eec9 100644 --- a/src/renderer/src/settings/ThemeSettingsTab.tsx +++ b/src/renderer/src/settings/ThemeSettingsTab.tsx @@ -32,6 +32,7 @@ export function ThemeSettingsTab({ const setCustomThemes = useSettingsStore((s) => s.setCustomThemes) const activeTheme = useResolvedTheme() const [opacityDraft, setOpacityDraft] = useState(null) + const [dimDraft, setDimDraft] = useState(null) const themeCards = useMemo( () => [...BUILTIN_THEMES, ...settings.customThemes], @@ -90,6 +91,26 @@ export function ThemeSettingsTab({ /> )} + {settings.terminal.backgroundImage !== '' && ( +
+
+
{t('settings.theme.backgroundImageDim')}
+
{t('settings.theme.backgroundImageDimDesc')}
+
+ setDimDraft(v)} + onChangeComplete={(v) => { + setDimDraft(null) + void updateTerminal({ backgroundImageDim: v }) + }} + /> +
+ )}
{t('settings.theme.accent')}
diff --git a/src/renderer/src/terminal/TerminalView.tsx b/src/renderer/src/terminal/TerminalView.tsx index 4e76f35..871f0a3 100644 --- a/src/renderer/src/terminal/TerminalView.tsx +++ b/src/renderer/src/terminal/TerminalView.tsx @@ -1400,6 +1400,14 @@ export const TerminalView: ForwardRefExoticComponent )} + {settings.terminal.backgroundImage !== '' && settings.terminal.backgroundImageDim > 0 && ( +
+ )} {dead && (
{t('terminal.dead.message', { code: exitCode })}
diff --git a/src/renderer/src/terminal/terminal.css b/src/renderer/src/terminal/terminal.css index adf6676..d8854b6 100644 --- a/src/renderer/src/terminal/terminal.css +++ b/src/renderer/src/terminal/terminal.css @@ -380,6 +380,17 @@ pointer-events: none; } +/* Black scrim between the background image and the xterm screen: dims the + image without washing it out (unlike opacity, which blends toward the dark + dock base), so bright images stay readable at any opacity. */ +.term-bg-dim { + position: absolute; + inset: 0; + z-index: 0; + background: #000; + pointer-events: none; +} + .terminal-view-dock > .xterm { position: relative; z-index: 1; diff --git a/src/shared/i18n/dicts/en/settings.ts b/src/shared/i18n/dicts/en/settings.ts index 315ede4..70d9205 100644 --- a/src/shared/i18n/dicts/en/settings.ts +++ b/src/shared/i18n/dicts/en/settings.ts @@ -288,6 +288,8 @@ const settings: Record = { 'settings.theme.chooseImage': 'Choose image', 'settings.theme.clearImage': 'Remove', 'settings.theme.imageOpacity': 'Image opacity', + 'settings.theme.backgroundImageDim': 'Background dim', + 'settings.theme.backgroundImageDimDesc': 'Overlays a black dim layer on top of the image; text is brightened automatically. 30–50% works well for bright images', 'settings.theme.customBadge': 'Custom', 'settings.theme.deleteTitle': 'Delete custom theme', 'settings.theme.create': 'New theme', diff --git a/src/shared/i18n/dicts/ja/settings.ts b/src/shared/i18n/dicts/ja/settings.ts index 508e366..2b8cc2e 100644 --- a/src/shared/i18n/dicts/ja/settings.ts +++ b/src/shared/i18n/dicts/ja/settings.ts @@ -283,6 +283,8 @@ const settings: Record = { 'settings.theme.chooseImage': '画像を選択', 'settings.theme.clearImage': '削除', 'settings.theme.imageOpacity': '画像の不透明度', + 'settings.theme.backgroundImageDim': '背景の減光', + 'settings.theme.backgroundImageDimDesc': '画像の上に黒い減光レイヤーを重ね、文字は自動で明るくなります。明るい画像は 30–50% が目安です', 'settings.theme.customBadge': 'カスタム', 'settings.theme.deleteTitle': 'カスタムテーマを削除', 'settings.theme.create': '新しいテーマ', diff --git a/src/shared/i18n/dicts/zh-CN/settings.ts b/src/shared/i18n/dicts/zh-CN/settings.ts index d05e644..b18f7a0 100644 --- a/src/shared/i18n/dicts/zh-CN/settings.ts +++ b/src/shared/i18n/dicts/zh-CN/settings.ts @@ -272,6 +272,8 @@ const settings: Record = { 'settings.theme.chooseImage': '选择图片', 'settings.theme.clearImage': '移除图片', 'settings.theme.imageOpacity': '图片不透明度', + 'settings.theme.backgroundImageDim': '背景压暗', + 'settings.theme.backgroundImageDimDesc': '在图片之上叠加黑色压暗层,文字随之自动提亮;亮图建议 30–50%', 'settings.theme.customBadge': '自定义', 'settings.theme.deleteTitle': '删除自定义主题', 'settings.theme.create': '新建主题', diff --git a/src/shared/i18n/dicts/zh-TW/settings.ts b/src/shared/i18n/dicts/zh-TW/settings.ts index 760ff15..0eaecbd 100644 --- a/src/shared/i18n/dicts/zh-TW/settings.ts +++ b/src/shared/i18n/dicts/zh-TW/settings.ts @@ -272,6 +272,8 @@ const settings: Record = { 'settings.theme.chooseImage': '選擇圖片', 'settings.theme.clearImage': '移除圖片', 'settings.theme.imageOpacity': '圖片不透明度', + 'settings.theme.backgroundImageDim': '背景壓暗', + 'settings.theme.backgroundImageDimDesc': '在圖片之上疊加黑色壓暗層,文字隨之自動提亮;亮圖建議 30–50%', 'settings.theme.customBadge': '自訂', 'settings.theme.deleteTitle': '刪除自訂主題', 'settings.theme.create': '新增主題', diff --git a/src/shared/settings.ts b/src/shared/settings.ts index 6c44172..7c69ae6 100644 --- a/src/shared/settings.ts +++ b/src/shared/settings.ts @@ -112,6 +112,8 @@ export interface TerminalSettings { backgroundImage: string /** background image layer opacity, 10..100 (%) */ backgroundImageOpacity: number + /** black scrim over the background image, under the text, 0..90 (%); 0 = off */ + backgroundImageDim: number } export type HighlightMode = 'all' | 'basic' | 'off' @@ -459,7 +461,8 @@ export const DEFAULT_SETTINGS: AppSettings = { highlightThemeColors: false, highlightPerHost: false, backgroundImage: '', - backgroundImageOpacity: 30 + backgroundImageOpacity: 30, + backgroundImageDim: 0 }, customThemes: [], highlightRules: DEFAULT_HIGHLIGHT_RULES,