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 <lawrencecchen@users.noreply.github.com>
This commit is contained in:
Lawrence Chen 2026-03-30 19:08:32 -07:00 committed by GitHub
parent 2d2d8da1c7
commit 543481ce12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 21 deletions

View file

@ -2500,14 +2500,13 @@ struct ContentView: View {
.contentShape(Rectangle()) .contentShape(Rectangle())
.background(TitlebarDoubleClickMonitorView()) .background(TitlebarDoubleClickMonitorView())
.background({ .background({
// The terminal area has two stacked semi-transparent layers: the Bonsplit // The terminal background is provided by a single CALayer
// container chrome background plus Ghostty's own Metal-rendered background. // (backgroundView in GhosttySurfaceScrollView), so the titlebar
// Compute the effective composited opacity so the titlebar matches visually. // opacity matches the configured value directly.
let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity) let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity)
let effective = alpha >= 0.999 ? alpha : 1.0 - pow(1.0 - alpha, 2)
return TitlebarLayerBackground( return TitlebarLayerBackground(
backgroundColor: GhosttyApp.shared.defaultBackgroundColor, backgroundColor: GhosttyApp.shared.defaultBackgroundColor,
opacity: effective opacity: alpha
) )
}()) }())
.overlay(alignment: .bottom) { .overlay(alignment: .bottom) {
@ -14022,14 +14021,13 @@ private struct SidebarBackdrop: View {
let cornerRadius = CGFloat(max(0, sidebarCornerRadius)) let cornerRadius = CGFloat(max(0, sidebarCornerRadius))
if matchTerminalBackground { if matchTerminalBackground {
// The terminal area has two stacked semi-transparent layers (Bonsplit chrome + // The terminal background is provided by a single CALayer, so
// Ghostty Metal background). Compute the effective composited opacity to match. // the sidebar uses the configured opacity directly.
let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity) let alpha = CGFloat(GhosttyApp.shared.defaultBackgroundOpacity)
let effective = alpha >= 0.999 ? alpha : 1.0 - pow(1.0 - alpha, 2)
return AnyView( return AnyView(
SidebarTerminalBackgroundView( SidebarTerminalBackgroundView(
backgroundColor: GhosttyApp.shared.defaultBackgroundColor, backgroundColor: GhosttyApp.shared.defaultBackgroundColor,
opacity: effective opacity: alpha
) )
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in .onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in

View file

@ -1303,6 +1303,12 @@ class GhosttyApp {
return return
} }
loadInlineGhosttyConfig(
"macos-background-from-layer = true",
into: fallbackConfig,
prefix: "cmux-layer-bg",
logLabel: "layer background (fallback)"
)
ghostty_config_finalize(fallbackConfig) ghostty_config_finalize(fallbackConfig)
updateDefaultBackground(from: fallbackConfig, source: "initialize.fallbackConfig") updateDefaultBackground(from: fallbackConfig, source: "initialize.fallbackConfig")
@ -1391,6 +1397,15 @@ class GhosttyApp {
userConfigDefinesShiftEnterBinding = Self.userConfigDefinesShiftEnterBinding() userConfigDefinesShiftEnterBinding = Self.userConfigDefinesShiftEnterBinding()
loadCopyOnSelectOverride(config) loadCopyOnSelectOverride(config)
loadCJKFontFallbackIfNeeded(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) ghostty_config_finalize(config)
} }
@ -7485,11 +7500,6 @@ final class GhosttySurfaceScrollView: NSView {
} }
private(set) var searchFocusTarget: SearchFocusTarget = .searchField 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 #if DEBUG
private var lastDropZoneOverlayLogSignature: String? private var lastDropZoneOverlayLogSignature: String?
@ -7677,9 +7687,8 @@ final class GhosttySurfaceScrollView: NSView {
backgroundView.wantsLayer = true backgroundView.wantsLayer = true
let initialTerminalBackground = GhosttyApp.shared.defaultBackgroundColor let initialTerminalBackground = GhosttyApp.shared.defaultBackgroundColor
.withAlphaComponent(GhosttyApp.shared.defaultBackgroundOpacity) .withAlphaComponent(GhosttyApp.shared.defaultBackgroundOpacity)
let initialPanelFill = Self.panelBackgroundFillColor(for: initialTerminalBackground) backgroundView.layer?.backgroundColor = initialTerminalBackground.cgColor
backgroundView.layer?.backgroundColor = initialPanelFill.cgColor backgroundView.layer?.isOpaque = initialTerminalBackground.alphaComponent >= 1.0
backgroundView.layer?.isOpaque = initialPanelFill.alphaComponent >= 1.0
addSubview(backgroundView) addSubview(backgroundView)
addSubview(scrollView) addSubview(scrollView)
inactiveOverlayView.wantsLayer = true inactiveOverlayView.wantsLayer = true
@ -8245,11 +8254,10 @@ final class GhosttySurfaceScrollView: NSView {
func setBackgroundColor(_ color: NSColor) { func setBackgroundColor(_ color: NSColor) {
guard let layer = backgroundView.layer else { return } guard let layer = backgroundView.layer else { return }
let fillColor = Self.panelBackgroundFillColor(for: color)
CATransaction.begin() CATransaction.begin()
CATransaction.setDisableActions(true) CATransaction.setDisableActions(true)
layer.backgroundColor = fillColor.cgColor layer.backgroundColor = color.cgColor
layer.isOpaque = fillColor.alphaComponent >= 1.0 layer.isOpaque = color.alphaComponent >= 1.0
CATransaction.commit() CATransaction.commit()
} }

View file

@ -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. 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 ## Upstreamed fork changes
### cursor-click-to-move respects OSC 133 click-to-move ### 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 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. 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. If you resolve a conflict, update this doc with what changed.

@ -1 +1 @@
Subproject commit bc9be90a21997a4e5f06bf15ae2ec0f937c2dc42 Subproject commit f9030b5c5232db69ba8625bb53d51ce735b80d51

View file

@ -8,3 +8,5 @@ c47010b80cd9ae6d1ab744c120f011a465521ea3 d6904870a3c920b2787b1c4b950cfdef232606b
312c7b23a7c8dc0704431940d76ba5dc32a46afb ae73cb18a9d6efec42126a1d99e0e9d12022403d7dc301dfa21ed9f7c89c9e30 312c7b23a7c8dc0704431940d76ba5dc32a46afb ae73cb18a9d6efec42126a1d99e0e9d12022403d7dc301dfa21ed9f7c89c9e30
404a3f175ba6baafabc46cac807194883e040980 bcbd2954f4746fe5bcb4bfca6efeddd3ea355fda2836371f4c7150271c58acbd 404a3f175ba6baafabc46cac807194883e040980 bcbd2954f4746fe5bcb4bfca6efeddd3ea355fda2836371f4c7150271c58acbd
bc9be90a21997a4e5f06bf15ae2ec0f937c2dc42 6b83b66768e8bba871a3753ae8ffbaabd03370b306c429cd86c9cdcc8db82589 bc9be90a21997a4e5f06bf15ae2ec0f937c2dc42 6b83b66768e8bba871a3753ae8ffbaabd03370b306c429cd86c9cdcc8db82589
41e796064e89eacabdf3a6729475e250a5518e7a 135302bbdf3e83b200f0165ff2a32cdf13017219e6c8ffca17b672edfbfae395
f9030b5c5232db69ba8625bb53d51ce735b80d51 6c439d731d97bd35a3289f54478af3ac01e30ba74ec2672490e0c98f95262b55