diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 259e40192c3a..1cc167b57e1d 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -2098,8 +2098,9 @@ class GhosttyApp { return } + let fallbackShouldUseHostLayerBackground = usesHostLayerBackground(for: fallbackConfig) loadInlineGhosttyConfig( - "macos-background-from-layer = true", + "macos-background-from-layer = \(fallbackShouldUseHostLayerBackground)", into: fallbackConfig, prefix: "cmux-renderer-bg", logLabel: "renderer background (fallback)" @@ -2112,7 +2113,7 @@ class GhosttyApp { ) loadCmuxOwnedGhosttyKeybindOverrides(fallbackConfig) let fallbackRenderingModeChanged = setUsesHostLayerBackground( - true, + fallbackShouldUseHostLayerBackground, source: "initialize.fallbackConfig" ) ghostty_config_finalize(fallbackConfig) @@ -2248,15 +2249,17 @@ class GhosttyApp { } #endif loadCJKFontFallbackIfNeeded(config) + let shouldUseHostLayerBackground = usesHostLayerBackground(for: config) let renderingModeChanged = setUsesHostLayerBackground( - true, + shouldUseHostLayerBackground, source: "loadDefaultConfigFilesWithLegacyFallback" ) - // Let cmux own the window-level backdrop once, while Ghostty keeps - // rendering text, cell backgrounds, and background images. This avoids - // separate translucent fills for terminal and chrome surfaces. + // Let Ghostty paint solid opaque terminal backgrounds so default cells + // and explicit ANSI background cells share one renderer/compositor path. + // Host-layer ownership remains required for translucent and blurred + // terminal backgrounds. loadInlineGhosttyConfig( - "macos-background-from-layer = true", + "macos-background-from-layer = \(shouldUseHostLayerBackground)", into: config, prefix: "cmux-renderer-bg", logLabel: "renderer background" @@ -3214,10 +3217,7 @@ class GhosttyApp { let resolvedCursorText = ghosttyColorValue(from: config, key: "cursor-text", fallback: baseline.cursorTextColor) let resolvedSelectionBackground = ghosttyColorValue(from: config, key: "selection-background", fallback: baseline.selectionBackground) let resolvedSelectionForeground = ghosttyColorValue(from: config, key: "selection-foreground", fallback: baseline.selectionForeground) - var opacity = baseline.backgroundOpacity - let opacityKey = "background-opacity" - _ = ghostty_config_get(config, &opacity, opacityKey, UInt(opacityKey.lengthOfBytes(using: .utf8))) - opacity = min(1.0, max(0.0, opacity)) + let opacity = defaultBackgroundOpacityValue(from: config) let backgroundBlur = defaultBackgroundBlurValue(from: config) applyDefaultBackground( color: resolvedColor, @@ -3234,6 +3234,20 @@ class GhosttyApp { ) } + private func defaultBackgroundOpacityValue(from config: ghostty_config_t) -> Double { + var opacity = Self.fallbackAppearanceConfig.backgroundOpacity + let key = "background-opacity" + _ = ghostty_config_get(config, &opacity, key, UInt(key.lengthOfBytes(using: .utf8))) + return Double(WindowAppearanceSnapshot.clampedOpacity(opacity)) + } + + private func usesHostLayerBackground(for config: ghostty_config_t) -> Bool { + WindowAppearanceSnapshot.usesHostLayerBackground( + backgroundOpacity: defaultBackgroundOpacityValue(from: config), + backgroundBlur: defaultBackgroundBlurValue(from: config) + ) + } + private func defaultBackgroundBlurValue(from config: ghostty_config_t) -> GhosttyBackgroundBlur { var value: Int16 = 0 let key = "background-blur" diff --git a/Sources/Windowing/WindowAppearanceSnapshot.swift b/Sources/Windowing/WindowAppearanceSnapshot.swift index 2b809560fc92..c3eac3a914af 100644 --- a/Sources/Windowing/WindowAppearanceSnapshot.swift +++ b/Sources/Windowing/WindowAppearanceSnapshot.swift @@ -213,6 +213,11 @@ struct WindowGlassSettingsSnapshot { } struct WindowAppearanceSnapshot { + /// Treat opacity values within floating-point round-trip tolerance of 1.0 + /// as opaque so user-configured `1.0` does not accidentally trigger host + /// ownership after Double/CGFloat conversions. + private static let opaqueBackgroundOwnershipThreshold: CGFloat = 0.999 + let terminalBackgroundColor: NSColor let terminalBackgroundOpacity: CGFloat let terminalBackgroundBlur: GhosttyBackgroundBlur @@ -285,6 +290,28 @@ struct WindowAppearanceSnapshot { usesHostLayerBackground ? .windowHostBackdrop : .ghosttyRendererOwnedBackgroundImage } + static func usesHostLayerBackground( + backgroundOpacity: Double, + backgroundBlur: GhosttyBackgroundBlur + ) -> Bool { + if backgroundBlur != .disabled { + return true + } + return clampedOpacity(backgroundOpacity) < opaqueBackgroundOwnershipThreshold + } + + static func terminalRenderingMode( + backgroundOpacity: Double, + backgroundBlur: GhosttyBackgroundBlur + ) -> GhosttyTerminalBackdropRenderingMode { + terminalRenderingMode( + usesHostLayerBackground: usesHostLayerBackground( + backgroundOpacity: backgroundOpacity, + backgroundBlur: backgroundBlur + ) + ) + } + var compositedTerminalBackgroundColor: NSColor { terminalBackgroundColor.withAlphaComponent(terminalBackgroundOpacity) } diff --git a/cmuxTests/WindowAppearanceSnapshotTests.swift b/cmuxTests/WindowAppearanceSnapshotTests.swift index 0396108065ec..53d55a0406a0 100644 --- a/cmuxTests/WindowAppearanceSnapshotTests.swift +++ b/cmuxTests/WindowAppearanceSnapshotTests.swift @@ -110,6 +110,54 @@ final class WindowAppearanceSnapshotTests: XCTestCase { XCTAssertEqual(plan.windowBackgroundColor.hexString(includeAlpha: true), "#272822FF") } + func testOpaqueUnblurredTerminalBackgroundIsRendererOwned() { + let snapshot = makeSnapshot( + unifySurfaceBackdrops: false, + backgroundOpacity: 1.0, + backgroundBlur: .disabled + ) + let policy = snapshot.policy(for: .windowRoot) + + XCTAssertNil(policy.hostLayerBackgroundColor) + guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else { + XCTFail("expected terminal backdrop policy") + return + } + XCTAssertEqual(renderingMode, .ghosttyRendererOwnedBackgroundImage) + } + + func testTranslucentTerminalBackgroundStaysHostLayerOwned() { + let snapshot = makeSnapshot( + unifySurfaceBackdrops: false, + backgroundOpacity: 0.9, + backgroundBlur: .disabled + ) + let policy = snapshot.policy(for: .windowRoot) + + XCTAssertNotNil(policy.hostLayerBackgroundColor) + guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else { + XCTFail("expected terminal backdrop policy") + return + } + XCTAssertEqual(renderingMode, .windowHostBackdrop) + } + + func testBlurredTerminalBackgroundStaysHostLayerOwned() { + let snapshot = makeSnapshot( + unifySurfaceBackdrops: false, + backgroundOpacity: 1.0, + backgroundBlur: .radius(20) + ) + let policy = snapshot.policy(for: .windowRoot) + + XCTAssertNotNil(policy.hostLayerBackgroundColor) + guard case let .ghosttyTerminalBackdrop(_, _, renderingMode) = policy else { + XCTFail("expected terminal backdrop policy") + return + } + XCTAssertEqual(renderingMode, .windowHostBackdrop) + } + func testDebugBackgroundGlassUsesWindowGlassPhase() { let snapshot = makeSnapshot( unifySurfaceBackdrops: false, @@ -137,7 +185,10 @@ final class WindowAppearanceSnapshotTests: XCTestCase { terminalBackgroundColor: NSColor(hex: "#272822") ?? .black, terminalBackgroundOpacity: backgroundOpacity, terminalBackgroundBlur: backgroundBlur, - terminalRenderingMode: .windowHostBackdrop, + terminalRenderingMode: WindowAppearanceSnapshot.terminalRenderingMode( + backgroundOpacity: Double(backgroundOpacity), + backgroundBlur: backgroundBlur + ), unifySurfaceBackdrops: unifySurfaceBackdrops, sidebarSettings: SidebarBackdropSettingsSnapshot( materialRawValue: SidebarMaterialOption.sidebar.rawValue,