mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 16:26:08 +03:00
153 lines
5.9 KiB
Swift
153 lines
5.9 KiB
Swift
import AppKit
|
|
|
|
/// A terminal window style that provides a transparent titlebar effect. With this effect, the titlebar
|
|
/// matches the background color of the window.
|
|
class TransparentTitlebarTerminalWindow: TerminalWindow {
|
|
/// Stores the last surface configuration to reapply appearance when needed.
|
|
/// This is necessary because various macOS operations (tab switching, tab bar
|
|
/// visibility changes) can reset the titlebar appearance.
|
|
private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig?
|
|
|
|
/// KVO observation for tab group window changes.
|
|
private var tabGroupWindowsObservation: NSKeyValueObservation?
|
|
private var tabBarVisibleObservation: NSKeyValueObservation?
|
|
|
|
override func awakeFromNib() {
|
|
super.awakeFromNib()
|
|
|
|
// Setup all the KVO we will use, see the docs for the respective functions
|
|
// to learn why we need KVO.
|
|
setupKVO()
|
|
}
|
|
|
|
deinit {
|
|
tabGroupWindowsObservation?.invalidate()
|
|
tabBarVisibleObservation?.invalidate()
|
|
}
|
|
|
|
override func becomeMain() {
|
|
super.becomeMain()
|
|
|
|
guard let lastSurfaceConfig else { return }
|
|
syncAppearance(lastSurfaceConfig)
|
|
|
|
// This is a nasty edge case. If we're going from 2 to 1 tab and the tab bar
|
|
// automatically disappears, then we need to resync our appearance because
|
|
// at some point macOS replaces the tab views.
|
|
if tabGroup?.windows.count ?? 0 == 2 {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in
|
|
self?.syncAppearance(self?.lastSurfaceConfig ?? lastSurfaceConfig)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Appearance
|
|
|
|
override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
|
super.syncAppearance(surfaceConfig)
|
|
|
|
// Save our config in case we need to reapply
|
|
lastSurfaceConfig = surfaceConfig
|
|
|
|
// Everytime we change appearance, set KVO up again in case any of our
|
|
// references changed (e.g. tabGroup is new).
|
|
setupKVO()
|
|
|
|
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 }
|
|
titlebarContainer.wantsLayer = true
|
|
titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor
|
|
}
|
|
|
|
// MARK: View Finders
|
|
|
|
private var titlebarBackgroundView: NSView? {
|
|
titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView")
|
|
}
|
|
|
|
// MARK: Tab Group Observation
|
|
|
|
private func setupKVO() {
|
|
// See the docs for the respective setup functions for why.
|
|
setupTabGroupObservation()
|
|
setupTabBarVisibleObservation()
|
|
}
|
|
|
|
/// Monitors the tabGroup windows value for any changes and resyncs the appearance on change.
|
|
/// This is necessary because when the windows change, the tab bar and titlebar are recreated
|
|
/// which breaks our changes.
|
|
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] _, change 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)
|
|
}
|
|
}
|
|
|
|
/// Monitors the tab bar for visibility. This lets the "Show/Hide Tab Bar" manual menu item
|
|
/// to not break our appearance.
|
|
private func setupTabBarVisibleObservation() {
|
|
// Remove existing observation if any
|
|
tabBarVisibleObservation?.invalidate()
|
|
tabBarVisibleObservation = nil
|
|
|
|
// Set up KVO observation for isTabBarVisible
|
|
tabBarVisibleObservation = tabGroup?.observe(
|
|
\.isTabBarVisible,
|
|
options: [.new]
|
|
) { [weak self] _, change in
|
|
guard let self else { return }
|
|
guard let lastSurfaceConfig else { return }
|
|
self.syncAppearance(lastSurfaceConfig)
|
|
}
|
|
}
|
|
}
|