fix(theme): review follow-ups — diff overlay background, hot-reload window effects, docs

- Diff overlay paints the gradient/opacity-aware window background instead
  of the stale representative solid.
- Config hot-reload now re-runs apply_theme with the window (blur flip,
  traffic-light re-pin) and re-syncs the Appearance opacity slider, so
  hand-edits to config.json / theme files take effect fully.
- Document the theme background/window settings in features docs (EN + zh-CN).
This commit is contained in:
l0ng-ai
2026-07-16 16:30:59 +08:00
parent 1a50cf0e60
commit 2dd817d8cd
7 changed files with 64 additions and 29 deletions
+3 -1
View File
@@ -17,7 +17,9 @@
- **Tabs & splits** — always open in the current directory
- **Command palette** <kbd>⌘ P</kbd> · scrollback search <kbd>⌘ F</kbd>
- **⌘-click links** · desktop notifications · copy on select (opt-in, Settings → Terminal → Clipboard)
- **Eight themes** · CJK / IME input
- **Eight themes, plus your own** — YAML seed themes with solid, gradient, or image backgrounds; iTerm2 `.itermcolors` import; in-app color editor with a background-image picker
- **Window opacity & blur** — Settings → Appearance → Window; applies to every theme, *Follow theme* returns to the theme's own `opacity` / `blur`
- **CJK / IME input**
## Coding agents
+3 -1
View File
@@ -17,7 +17,9 @@
- **标签页与分屏** —— 永远开在当前目录
- **命令面板** <kbd>⌘ P</kbd> · 回滚搜索 <kbd>⌘ F</kbd>
- **⌘ 点击打开链接** · 桌面通知 · 划选即复制(可选,设置 → 终端 → 剪贴板)
- **8 套主题** · CJK / 输入法输入
- **8 套主题,也能自定义** — YAML 种子主题,背景支持纯色、渐变或图片;可导入 iTerm2 `.itermcolors`;应用内颜色编辑器带背景图选择
- **窗口透明与模糊** — 设置 → Appearance → Window;对所有主题生效,*Follow theme* 恢复主题自带的 `opacity` / `blur`
- **CJK / 输入法输入**
## Coding agent
+5 -4
View File
@@ -137,8 +137,10 @@ fn spawn_config_watcher(cx: &mut App) {
// Re-paint theme + colors from the new config. We have no window
// handle in this global task, but `apply_theme` accepts `None`:
// it still updates the `Theme`/palette globals (what actually
// repaints); the only thing it skips is re-pinning the macOS
// traffic lights, which self-corrects on the next resize/activate.
// repaints). The window-bound effects — the Transparent↔Blurred
// background flip and traffic-light re-pinning — are covered by
// `Tty7App::reload_from_config`, which observes the `Config`
// global with its window and re-runs `apply_theme(Some(window))`.
crate::ui::theme::apply_theme(None, cx);
// Schedule every window to redraw so the new palette shows at once.
cx.refresh_windows();
@@ -380,8 +382,7 @@ fn main() {
WindowBounds::Fullscreen(bounds)
}
};
let window_background =
cx.update(|cx| crate::ui::theme::background_appearance(cx));
let window_background = cx.update(|cx| crate::ui::theme::background_appearance(cx));
let options = WindowOptions {
window_bounds: Some(window_bounds),
// Start from the component defaults but nudge the traffic lights
+33 -14
View File
@@ -482,10 +482,13 @@ impl Tty7App {
let mf_description = cx.new(|cx| InputState::new(window, cx).placeholder("description"));
let sidebar_width = cx.global::<Config>().sidebar_width;
// Live-apply hot-reloaded config: the watcher in `main.rs` swaps the
// `Config` global on every `config.json` change, which fires this. Theme
// and colors are handled separately by `apply_theme`; here we cover the
// font knobs that live on `Tty7App`/the panes.
let config_watch = cx.observe_global::<Config>(|this, cx| this.reload_from_config(cx));
// `Config` global on every `config.json` change, which fires this. The
// window-aware variant so the reload can re-run `apply_theme` with the
// window (blur flip, traffic-light pinning) and re-sync the Appearance
// opacity slider — the watcher task itself has no window handle.
let config_watch = cx.observe_global_in::<Config>(window, |this, window, cx| {
this.reload_from_config(window, cx)
});
// Repaint when any pane's git probe lands in the shared cache — the
// sidebar's branch/diff lines read from it, and the probing pane's own
// notify wouldn't re-render rows belonging to *other* panes. The open
@@ -1014,7 +1017,12 @@ impl Tty7App {
}
/// Set the global window-blur override from the Appearance switch.
pub(crate) fn set_window_blur(&mut self, on: bool, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn set_window_blur(
&mut self,
on: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
cx.global_mut::<Config>().window_blur = Some(on);
apply_theme(Some(window), cx);
cx.global::<Config>().save();
@@ -1038,7 +1046,11 @@ impl Tty7App {
/// Snap the Appearance opacity slider's thumb to the value now in effect.
/// Needed whenever that value changes for a reason other than the user
/// dragging it (theme switch, "Follow theme" reset).
pub(crate) fn sync_window_opacity_slider(&mut self, window: &mut Window, cx: &mut Context<Self>) {
pub(crate) fn sync_window_opacity_slider(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) {
let eff = Self::effective_window_opacity(cx);
if let Some(slider) = self
.active_settings()
@@ -2969,15 +2981,13 @@ impl Tty7App {
.step(0.01)
.default_value(eff)
});
subs.push(cx.subscribe_in(
&slider,
window,
|this, _s, ev: &SliderEvent, window, cx| {
subs.push(
cx.subscribe_in(&slider, window, |this, _s, ev: &SliderEvent, window, cx| {
if let SliderEvent::Change(v) = ev {
this.set_window_opacity(v.start(), window, cx);
}
},
));
}),
);
slider
}
@@ -3135,7 +3145,14 @@ impl Tty7App {
/// *our own* code mutated the global (every font setter and `set_preset`
/// writes it), and — because we never write the global or `save()` from here
/// — closes the save → watch → reload loop that would otherwise oscillate.
fn reload_from_config(&mut self, cx: &mut Context<Self>) {
fn reload_from_config(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Re-apply the theme with the window in hand: the watcher task calls
// `apply_theme(None)` (colors/palette), but the window-bound effects — the
// Transparent↔Blurred background flip and traffic-light re-pinning — only
// happen here. Also keeps the Appearance opacity slider's thumb on a value
// that was hand-edited in `config.json` or the theme file.
apply_theme(Some(window), cx);
self.sync_window_opacity_slider(window, cx);
let config = cx.global::<Config>().clone();
if config.cursor_style != self.terminal_cursor_style
|| config.scrollback_limit != self.terminal_scrollback_limit
@@ -3954,7 +3971,9 @@ impl Render for Tty7App {
// Same gradient-aware paint as the root, so a gradient theme's
// settings page doesn't snap to a flat color. A translucent
// theme's alpha rides along, letting the background image show
// through here too.
// through here too. This second layer compounds the alpha over
// the root's paint — deliberate: the overlay must occlude the
// terminal behind it to stay readable.
.bg(window_bg)
.child(self.render_settings(cx))
});
+11 -1
View File
@@ -219,7 +219,17 @@ impl Tty7App {
.inset_0()
// Blocks mouse from reaching the terminal underneath.
.occlude()
.bg(cx.theme().background)
// Same gradient/opacity-aware paint as the root and the settings
// overlay, so a gradient or image theme doesn't snap to a flat
// color here. On a translucent theme this second layer compounds
// the alpha a little — deliberate: the overlay must occlude the
// terminal behind it to stay readable.
.bg(
match cx.try_global::<crate::ui::presets::ActiveBackground>() {
Some(bg) => crate::ui::theme::window_background(bg),
None => cx.theme().background.into(),
},
)
.text_color(cx.theme().foreground)
.track_focus(&overlay.focus_handle)
.on_key_down(cx.listener(|this, ev: &KeyDownEvent, window, cx| {
+8 -4
View File
@@ -1263,9 +1263,9 @@ impl Tty7App {
.into_any_element();
let blur_switch = Switch::new("window-blur")
.checked(blur)
.on_click(cx.listener(|this, on: &bool, window, cx| {
this.set_window_blur(*on, window, cx)
}))
.on_click(
cx.listener(|this, on: &bool, window, cx| this.set_window_blur(*on, window, cx)),
)
.into_any_element();
v_flex()
@@ -1341,7 +1341,11 @@ impl Tty7App {
.w(px(240.))
.child(
Button::new("pick-theme-image")
.label(if image.is_some() { "Change…" } else { "Choose…" })
.label(if image.is_some() {
"Change…"
} else {
"Choose…"
})
.small()
.on_click(cx.listener(|this, _, _w, cx| this.pick_theme_image(cx))),
)
+1 -4
View File
@@ -117,10 +117,7 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
// Window opacity / blur: the global config override wins when set (so a
// chosen translucency survives theme switches); otherwise the theme's own
// values apply. Only an opacity below 1.0 makes the window translucent.
let opacity = config
.window_opacity
.or(theme.opacity)
.filter(|o| *o < 1.0);
let opacity = config.window_opacity.or(theme.opacity).filter(|o| *o < 1.0);
let blur = config.window_blur.unwrap_or(theme.blur);
// Force the native macOS chrome (traffic lights, system menus, scrollbars)
// into the theme's own light/dark mode regardless of the OS setting.