ghostty/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift
2025-06-11 15:18:02 -07:00

142 lines
5.3 KiB
Swift

import AppKit
class TransparentTitlebarTerminalWindow: TerminalWindow {
// We need to restore our last synced appearance so that we can reapply
// the appearance in certain scenarios.
private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig?
// KVO observations
private var tabGroupWindowsObservation: NSKeyValueObservation?
override func awakeFromNib() {
super.awakeFromNib()
// We need to observe the tab group because we need to redraw on
// tabbed window changes and there is no notification for that.
setupTabGroupObservation()
}
deinit {
tabGroupWindowsObservation?.invalidate()
}
override func becomeMain() {
// On macOS Tahoe, the tab bar redraws and restores non-transparency when
// switching tabs. To overcome this, we resync the appearance whenever this
// window becomes main (focused).
if #available(macOS 26.0, *),
let lastSurfaceConfig {
syncAppearance(lastSurfaceConfig)
}
}
// MARK: Appearance
override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
super.syncAppearance(surfaceConfig)
lastSurfaceConfig = surfaceConfig
if #available(macOS 26.0, *) {
syncAppearanceTahoe(surfaceConfig)
} else {
syncAppearanceVentura(surfaceConfig)
}
}
@available(macOS 26.0, *)
private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
// When we have transparency, we need to set the titlebar background to match the
// window background but with opacity. The window background is set using the
// "preferred background color" property.
//
// As an inverse, if we don't have transparency, we don't bother with this because
// the window background will be set to the correct color so we can just hide the
// titlebar completely and we're good to go.
if !isOpaque {
if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") {
titlebarView.wantsLayer = true
titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor
}
}
// In all cases, we have to hide the background view since this has multiple subviews
// that force a background color.
titlebarBackgroundView?.isHidden = true
}
@available(macOS 13.0, *)
private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
guard let titlebarContainer else { return }
let configBgColor = NSColor(surfaceConfig.backgroundColor)
// Set our window background color so it shows up
backgroundColor = configBgColor
// Set the background color of our titlebar to match
titlebarContainer.wantsLayer = true
titlebarContainer.layer?.backgroundColor = configBgColor.withAlphaComponent(surfaceConfig.backgroundOpacity).cgColor
}
// MARK: View Finders
private var titlebarBackgroundView: NSView? {
titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView")
}
private var titlebarContainer: NSView? {
// If we aren't fullscreen then the titlebar container is part of our window.
if !styleMask.contains(.fullScreen) {
return titlebarContainerView
}
// If we are fullscreen, the titlebar container view is part of a separate
// "fullscreen window", we need to find the window and then get the view.
for window in NSApplication.shared.windows {
// This is the private window class that contains the toolbar
guard window.className == "NSToolbarFullScreenWindow" else { continue }
// The parent will match our window. This is used to filter the correct
// fullscreen window if we have multiple.
guard window.parent == self else { continue }
return titlebarContainerView
}
return nil
}
private var titlebarContainerView: NSView? {
contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView")
}
// MARK: Tab Group Observation
private func setupTabGroupObservation() {
// Remove existing observation if any
tabGroupWindowsObservation?.invalidate()
tabGroupWindowsObservation = nil
// Check if tabGroup is available
guard let tabGroup else { return }
// Set up KVO observation for the windows array. Whenever it changes
// we resync the appearance because it can cause macOS to redraw the
// tab bar.
tabGroupWindowsObservation = tabGroup.observe(
\.windows,
options: [.new]
) { [weak self] _, _ in
// NOTE: At one point, I guarded this on only if we went from 0 to N
// or N to 0 under the assumption that the tab bar would only get
// replaced on those cases. This turned out to be false (Tahoe).
// It's cheap enough to always redraw this so we should just do it
// unconditionally.
guard let self else { return }
guard let lastSurfaceConfig else { return }
self.syncAppearance(lastSurfaceConfig)
}
}
}