diff --git a/docs/features.md b/docs/features.md index 00d8aa85..d854bfe7 100644 --- a/docs/features.md +++ b/docs/features.md @@ -17,7 +17,9 @@ - **Tabs & splits** — always open in the current directory - **Command palette** ⌘ P · scrollback search ⌘ F - **⌘-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 diff --git a/docs/features.zh-CN.md b/docs/features.zh-CN.md index 48851018..cd66696b 100644 --- a/docs/features.zh-CN.md +++ b/docs/features.zh-CN.md @@ -17,7 +17,9 @@ - **标签页与分屏** —— 永远开在当前目录 - **命令面板** ⌘ P · 回滚搜索 ⌘ F - **⌘ 点击打开链接** · 桌面通知 · 划选即复制(可选,设置 → 终端 → 剪贴板) -- **8 套主题** · CJK / 输入法输入 +- **8 套主题,也能自定义** — YAML 种子主题,背景支持纯色、渐变或图片;可导入 iTerm2 `.itermcolors`;应用内颜色编辑器带背景图选择 +- **窗口透明与模糊** — 设置 → Appearance → Window;对所有主题生效,*Follow theme* 恢复主题自带的 `opacity` / `blur` +- **CJK / 输入法输入** ## Coding agent diff --git a/src/main.rs b/src/main.rs index 788e7d11..13a48a18 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 diff --git a/src/ui/app.rs b/src/ui/app.rs index bb372f08..09dc39a6 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -482,10 +482,13 @@ impl Tty7App { let mf_description = cx.new(|cx| InputState::new(window, cx).placeholder("description")); let sidebar_width = cx.global::().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::(|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::(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) { + pub(crate) fn set_window_blur( + &mut self, + on: bool, + window: &mut Window, + cx: &mut Context, + ) { cx.global_mut::().window_blur = Some(on); apply_theme(Some(window), cx); cx.global::().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) { + pub(crate) fn sync_window_opacity_slider( + &mut self, + window: &mut Window, + cx: &mut Context, + ) { 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) { + fn reload_from_config(&mut self, window: &mut Window, cx: &mut Context) { + // 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::().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)) }); diff --git a/src/ui/diff_overlay.rs b/src/ui/diff_overlay.rs index 4f1a8f0f..65e0d413 100644 --- a/src/ui/diff_overlay.rs +++ b/src/ui/diff_overlay.rs @@ -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::() { + 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| { diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 9beed4a8..12960419 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -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))), ) diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 8fb837d2..eee5e9e2 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -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.