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())
.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

View file

@ -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()
}