From 543481ce1299af88c955c3ca30d60a0e69a2d93e Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Mon, 30 Mar 2026 19:08:32 -0700 Subject: [PATCH] Fix transparent background flash during sidebar toggle (#2378) * Fix transparent background flash during sidebar toggle Move terminal background rendering from the Metal GPU pass to a CALayer (backgroundView). The GPU bg_color pass is disabled via a new Ghostty config flag (macos-background-from-layer). The CALayer resizes instantly with its parent NSView, eliminating the 3-5 frame gap where the desktop was visible through the transparent window during sidebar toggles and layout transitions. Also simplifies the titlebar and sidebar opacity formulas since there is now a single background layer instead of two stacked semi-transparent layers. * Document macos-background-from-layer fork change * Pin GhosttyKit checksum for macos-background-from-layer * Address review feedback: fix fallback config path, inline identity wrapper - Inject macos-background-from-layer in the fallback config path too, preventing alpha double-stacking when user config is invalid - Inline panelBackgroundFillColor (now an identity function) at its two call sites and remove the wrapper * Address adversarial review: skip fullscreen bg draw call explicitly The bg_color uniform alpha is still zeroed for cell compositing (so transparent cells pass through to the CALayer), but the fullscreen background fill draw step is now explicitly skipped instead of relying on alpha=0 as a no-op. * Pin GhosttyKit checksum for bg draw-call skip --------- Co-authored-by: Lawrence Chen --- Sources/ContentView.swift | 16 +++++++--------- Sources/GhosttyTerminalView.swift | 30 +++++++++++++++++++----------- docs/ghostty-fork.md | 16 ++++++++++++++++ ghostty | 2 +- scripts/ghosttykit-checksums.txt | 2 ++ 5 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 8145ea58..248dc2f3 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -2500,14 +2500,13 @@ struct ContentView: View { .contentShape(Rectangle()) .background(TitlebarDoubleClickMonitorView()) .background({ - // The terminal area has two stacked semi-transparent layers: the Bonsplit - // container chrome background plus Ghostty's own Metal-rendered background. - // Compute the effective composited opacity so the titlebar matches visually. + // The terminal background is provided by a single CALayer + // (backgroundView in GhosttySurfaceScrollView), so the titlebar + // opacity matches the configured value directly. let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity) - let effective = alpha >= 0.999 ? alpha : 1.0 - pow(1.0 - alpha, 2) return TitlebarLayerBackground( backgroundColor: GhosttyApp.shared.defaultBackgroundColor, - opacity: effective + opacity: alpha ) }()) .overlay(alignment: .bottom) { @@ -14022,14 +14021,13 @@ private struct SidebarBackdrop: View { let cornerRadius = CGFloat(max(0, sidebarCornerRadius)) if matchTerminalBackground { - // The terminal area has two stacked semi-transparent layers (Bonsplit chrome + - // Ghostty Metal background). Compute the effective composited opacity to match. + // The terminal background is provided by a single CALayer, so + // the sidebar uses the configured opacity directly. let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity) - let effective = alpha >= 0.999 ? alpha : 1.0 - pow(1.0 - alpha, 2) return AnyView( SidebarTerminalBackgroundView( backgroundColor: GhosttyApp.shared.defaultBackgroundColor, - opacity: effective + opacity: alpha ) .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index e7027e80..d7d920aa 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -1303,6 +1303,12 @@ class GhosttyApp { return } + loadInlineGhosttyConfig( + "macos-background-from-layer = true", + into: fallbackConfig, + prefix: "cmux-layer-bg", + logLabel: "layer background (fallback)" + ) ghostty_config_finalize(fallbackConfig) updateDefaultBackground(from: fallbackConfig, source: "initialize.fallbackConfig") @@ -1391,6 +1397,15 @@ class GhosttyApp { userConfigDefinesShiftEnterBinding = Self.userConfigDefinesShiftEnterBinding() loadCopyOnSelectOverride(config) loadCJKFontFallbackIfNeeded(config) + // cmux provides the terminal background via backgroundView (CALayer) + // instead of the GPU full-screen bg pass, so the layer can provide + // instant coverage during sidebar toggle and other layout transitions. + loadInlineGhosttyConfig( + "macos-background-from-layer = true", + into: config, + prefix: "cmux-layer-bg", + logLabel: "layer background" + ) ghostty_config_finalize(config) } @@ -7485,11 +7500,6 @@ final class GhosttySurfaceScrollView: NSView { } private(set) var searchFocusTarget: SearchFocusTarget = .searchField - private static func panelBackgroundFillColor(for terminalBackgroundColor: NSColor) -> NSColor { - // The Ghostty renderer already draws translucent terminal backgrounds. If we paint an - // additional translucent layer here, alpha stacks and appears effectively opaque. - terminalBackgroundColor.alphaComponent < 0.999 ? .clear : terminalBackgroundColor - } #if DEBUG private var lastDropZoneOverlayLogSignature: String? @@ -7677,9 +7687,8 @@ final class GhosttySurfaceScrollView: NSView { backgroundView.wantsLayer = true let initialTerminalBackground = GhosttyApp.shared.defaultBackgroundColor .withAlphaComponent(GhosttyApp.shared.defaultBackgroundOpacity) - let initialPanelFill = Self.panelBackgroundFillColor(for: initialTerminalBackground) - backgroundView.layer?.backgroundColor = initialPanelFill.cgColor - backgroundView.layer?.isOpaque = initialPanelFill.alphaComponent >= 1.0 + backgroundView.layer?.backgroundColor = initialTerminalBackground.cgColor + backgroundView.layer?.isOpaque = initialTerminalBackground.alphaComponent >= 1.0 addSubview(backgroundView) addSubview(scrollView) inactiveOverlayView.wantsLayer = true @@ -8245,11 +8254,10 @@ final class GhosttySurfaceScrollView: NSView { func setBackgroundColor(_ color: NSColor) { guard let layer = backgroundView.layer else { return } - let fillColor = Self.panelBackgroundFillColor(for: color) CATransaction.begin() CATransaction.setDisableActions(true) - layer.backgroundColor = fillColor.cgColor - layer.isOpaque = fillColor.alphaComponent >= 1.0 + layer.backgroundColor = color.cgColor + layer.isOpaque = color.alphaComponent >= 1.0 CATransaction.commit() } diff --git a/docs/ghostty-fork.md b/docs/ghostty-fork.md index 49de9988..55cc1207 100644 --- a/docs/ghostty-fork.md +++ b/docs/ghostty-fork.md @@ -102,6 +102,17 @@ The fork branch HEAD is now the section 6 zsh redraw follow-up commit. The fork branch HEAD is now the section 7 cmux theme picker helper commit. +### 8) macos-background-from-layer config flag + +- Branch: `feat-layer-bg` +- Files: + - `src/config/Config.zig` + - `src/renderer/generic.zig` +- Summary: + - Adds a `macos-background-from-layer` bool config (default false). + - When true, sets `bg_color[3] = 0` in the per-frame uniform update so the Metal renderer skips the full-screen background fill. + - Allows the host app to provide the terminal background via `CALayer.backgroundColor` for instant coverage during view resizes, avoiding alpha double-stacking. + ## Upstreamed fork changes ### cursor-click-to-move respects OSC 133 click-to-move @@ -130,4 +141,9 @@ These files change frequently upstream; be careful when rebasing the fork: If upstream reorganizes the preview loop or key handling, re-check the cmux mode path and keep the stock Ghostty behavior unchanged when the cmux env vars are absent. +- `src/renderer/generic.zig` + - The `macos-background-from-layer` check sits next to the glass-style check in `updateFrame`. + If upstream refactors the bg_color uniform update or the glass conditional, re-check that both + paths still zero out `bg_color[3]` correctly. + If you resolve a conflict, update this doc with what changed. diff --git a/ghostty b/ghostty index bc9be90a..f9030b5c 160000 --- a/ghostty +++ b/ghostty @@ -1 +1 @@ -Subproject commit bc9be90a21997a4e5f06bf15ae2ec0f937c2dc42 +Subproject commit f9030b5c5232db69ba8625bb53d51ce735b80d51 diff --git a/scripts/ghosttykit-checksums.txt b/scripts/ghosttykit-checksums.txt index 1e45a32a..0d5946e9 100644 --- a/scripts/ghosttykit-checksums.txt +++ b/scripts/ghosttykit-checksums.txt @@ -8,3 +8,5 @@ c47010b80cd9ae6d1ab744c120f011a465521ea3 d6904870a3c920b2787b1c4b950cfdef232606b 312c7b23a7c8dc0704431940d76ba5dc32a46afb ae73cb18a9d6efec42126a1d99e0e9d12022403d7dc301dfa21ed9f7c89c9e30 404a3f175ba6baafabc46cac807194883e040980 bcbd2954f4746fe5bcb4bfca6efeddd3ea355fda2836371f4c7150271c58acbd bc9be90a21997a4e5f06bf15ae2ec0f937c2dc42 6b83b66768e8bba871a3753ae8ffbaabd03370b306c429cd86c9cdcc8db82589 +41e796064e89eacabdf3a6729475e250a5518e7a 135302bbdf3e83b200f0165ff2a32cdf13017219e6c8ffca17b672edfbfae395 +f9030b5c5232db69ba8625bb53d51ce735b80d51 6c439d731d97bd35a3289f54478af3ac01e30ba74ec2672490e0c98f95262b55