macOS: rework for #10943

Keep the background color as it is and apply glass effect on top of it
This commit is contained in:
Lukas
2026-08-20 12:38:55 +02:00
parent 9ae02a326f
commit fd17869d15
6 changed files with 46 additions and 120 deletions

View File

@@ -159,8 +159,6 @@ class QuickTerminalController: BaseTerminalController {
// applies if we can be seen.
guard visible else { return }
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: true)
// Re-hide the dock if we were hiding it before.
hiddenDock?.hide()
}
@@ -174,8 +172,6 @@ class QuickTerminalController: BaseTerminalController {
// ensures we don't run logic twice.
guard visible else { return }
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: false)
// We don't animate out if there is a modal sheet being shown currently.
// This lets us show alerts without causing the window to disappear.
guard window?.attachedSheet == nil else { return }

View File

@@ -1239,12 +1239,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
super.windowDidBecomeKey(notification)
self.relabelTabs()
self.fixTabBar()
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: true)
}
override func windowDidResignKey(_ notification: Notification) {
super.windowDidResignKey(notification)
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: false)
}
override func windowDidMove(_ notification: Notification) {

View File

@@ -6,7 +6,7 @@ import SwiftUI
class TerminalViewContainer: NSView {
private let terminalView: NSView
/// Combined glass effect and inactive tint overlay view
/// Background color applied with glass effect
private(set) var glassEffectView: NSView?
private var derivedConfig: DerivedConfig?
@@ -96,14 +96,42 @@ extension BaseTerminalController {
/// an inactive-window tint overlay.
#if compiler(>=6.2)
@available(macOS 26.0, *)
private class TerminalGlassView: NSView {
private let glassEffectView: NSGlassEffectView
private class TerminalGlassView: NSView, ObservableObject {
/// We use this to apply glass effect to background colors
///
struct GlassBackground: View {
@ObservedObject var model: GlassViewModel
var body: some View {
model.color
.glassEffect(
model.glass,
in: RoundedRectangle(cornerRadius: model.cornerRadius)
)
}
}
class GlassViewModel: ObservableObject {
@Published var isActive: Bool = false
@Published var backgroundColor: Color = .clear
@Published var backgroundOpacity: Double = 0
@Published var cornerRadius: CGFloat = 0
@Published var glass: Glass = .identity
/// backgroundColor applied with backgroundOpacity
var color: Color {
backgroundColor.opacity(backgroundOpacity)
}
}
private let glassEffectView: NSView
private var topConstraint: NSLayoutConstraint!
private let tintOverlay: NSView
private let glassViewModel: GlassViewModel
init(topOffset: CGFloat) {
self.glassEffectView = NSGlassEffectView()
self.tintOverlay = NSView()
let viewModel = GlassViewModel()
self.glassEffectView = NSHostingView(rootView: GlassBackground(model: viewModel))
self.glassViewModel = viewModel
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
@@ -121,19 +149,6 @@ private class TerminalGlassView: NSView {
glassEffectView.bottomAnchor.constraint(equalTo: bottomAnchor),
glassEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
// Tint overlay sits above the glass effect.
tintOverlay.translatesAutoresizingMaskIntoConstraints = false
tintOverlay.wantsLayer = true
tintOverlay.alphaValue = 0
addSubview(tintOverlay, positioned: .above, relativeTo: glassEffectView)
NSLayoutConstraint.activate([
tintOverlay.topAnchor.constraint(equalTo: glassEffectView.topAnchor),
tintOverlay.leadingAnchor.constraint(equalTo: glassEffectView.leadingAnchor),
tintOverlay.bottomAnchor.constraint(equalTo: glassEffectView.bottomAnchor),
tintOverlay.trailingAnchor.constraint(equalTo: glassEffectView.trailingAnchor),
])
}
@available(*, unavailable)
@@ -141,19 +156,17 @@ private class TerminalGlassView: NSView {
fatalError("init(coder:) has not been implemented")
}
/// Configures the glass effect style, tint color, corner radius, and
/// updates the inactive tint overlay based on window key status.
/// Configures the glass, tint color, corner radius.
func configure(
style: NSGlassEffectView.Style,
glass: Glass,
backgroundColor: NSColor,
backgroundOpacity: Double,
cornerRadius: CGFloat?,
isKeyWindow: Bool
) {
glassEffectView.style = style
glassEffectView.tintColor = backgroundColor.withAlphaComponent(backgroundOpacity)
glassEffectView.cornerRadius = cornerRadius ?? 0
updateKeyStatus(isKeyWindow, backgroundColor: backgroundColor)
glassViewModel.backgroundColor = Color(backgroundColor)
glassViewModel.backgroundOpacity = backgroundOpacity
glassViewModel.cornerRadius = cornerRadius ?? 0
glassViewModel.glass = glass
}
/// Updates the top inset offset for both the glass effect and tint overlay.
@@ -161,21 +174,6 @@ private class TerminalGlassView: NSView {
func updateTopInset(_ offset: CGFloat) {
topConstraint.constant = offset
}
/// Updates the tint overlay visibility based on window key status.
func updateKeyStatus(_ isKeyWindow: Bool, backgroundColor: NSColor) {
let tint = tintProperties(for: backgroundColor)
tintOverlay.layer?.backgroundColor = tint.color.cgColor
tintOverlay.alphaValue = isKeyWindow ? 0 : tint.opacity
}
/// Computes a saturation-boosted tint color and opacity for the inactive overlay.
private func tintProperties(for color: NSColor) -> (color: NSColor, opacity: CGFloat) {
let isLight = color.isLightColor
let vibrant = color.adjustingSaturation(by: 1.2)
let overlayOpacity: CGFloat = isLight ? 0.35 : 0.85
return (vibrant, overlayOpacity)
}
}
#endif // compiler(>=6.2)
@@ -215,11 +213,10 @@ extension TerminalViewContainer {
}
effectView.configure(
style: derivedConfig.style.official,
glass: derivedConfig.glass.official,
backgroundColor: derivedConfig.backgroundColor,
backgroundOpacity: derivedConfig.backgroundOpacity,
cornerRadius: derivedConfig.cornerRadius,
isKeyWindow: window?.isKeyWindow ?? true
)
#endif // compiler(>=6.2)
}
@@ -237,21 +234,8 @@ extension TerminalViewContainer {
#endif // compiler(>=6.2)
}
func updateGlassTintOverlay(isKeyWindow: Bool) {
#if compiler(>=6.2)
guard
#available(macOS 26.0, *),
let effectView = glassEffectView as? TerminalGlassView,
let derivedConfig
else {
return
}
effectView.updateKeyStatus(isKeyWindow, backgroundColor: derivedConfig.backgroundColor)
#endif // compiler(>=6.2)
}
struct DerivedConfig: Equatable {
let style: BackportNSGlassStyle
let glass: BackportGlass
let backgroundColor: NSColor
let backgroundOpacity: Double
let cornerRadius: CGFloat?
@@ -259,9 +243,9 @@ extension TerminalViewContainer {
init?(config: Ghostty.Config, preferredBackgroundColor: NSColor?, cornerRadius: CGFloat?) {
switch config.backgroundBlur {
case .macosGlassRegular:
style = .regular
glass = .regular
case .macosGlassClear:
style = .clear
glass = .clear
default:
return nil
}

View File

@@ -104,11 +104,11 @@ enum BackportPointerStyle {
}
}
enum BackportNSGlassStyle {
enum BackportGlass {
case regular, clear
@available(macOS 26, *)
var official: NSGlassEffectView.Style {
var official: Glass {
switch self {
case .regular: return .regular
case .clear: return .clear

View File

@@ -24,14 +24,6 @@ extension NSColor {
appleColorList?.allKeys.map { $0.lowercased() } ?? []
}
/// Returns a new color with its saturation multiplied by the given factor, clamped to [0, 1].
func adjustingSaturation(by factor: CGFloat) -> NSColor {
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
let hsbColor = self.usingColorSpace(.sRGB) ?? self
hsbColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
return NSColor(hue: h, saturation: min(max(s * factor, 0), 1), brightness: b, alpha: a)
}
/// Calculates the perceptual distance to another color in RGB space.
func distance(to other: NSColor) -> Double {
guard let a = self.usingColorSpace(.sRGB),

View File

@@ -60,44 +60,4 @@ struct TerminalViewContainerTests {
#expect(view.glassEffectView == nil)
}
}
#if compiler(>=6.2)
@Test func configChangeUpdatesGlass() async throws {
guard #available(macOS 26.0, *) else { return }
let view = await MockTerminalViewContainer {
EmptyView()
}
let config1 = MockConfig(backgroundBlur: .macosGlassRegular, backgroundColor: .clear, backgroundOpacity: 1)
await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: nil)
let glassEffectView = await view.descendants(withClassName: "NSGlassEffectView").first as? NSGlassEffectView
let effectView = try #require(glassEffectView)
try await Task.sleep(nanoseconds: UInt64(1e8)) // wait for the view to be setup if needed
#expect(effectView.tintColor?.hexString == NSColor.clear.hexString)
// Test with same config but with different preferredBackgroundColor
await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red)
#expect(effectView.tintColor?.hexString == NSColor.red.hexString)
// MARK: - Corner Radius
#expect(effectView.cornerRadius == 0)
await MainActor.run { view._windowCornerRadius = 10 }
// This won't change, unless ghosttyConfigDidChange is called
#expect(effectView.cornerRadius == 0)
await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red)
#expect(effectView.cornerRadius == 10)
// MARK: - Glass Style
#expect(effectView.style == .regular)
let config2 = MockConfig(backgroundBlur: .macosGlassClear, backgroundColor: .clear, backgroundOpacity: 1)
await view.ghosttyConfigDidChange(config2, preferredBackgroundColor: .red)
#expect(effectView.style == .clear)
}
#endif // compiler(>=6.2)
}