mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00

Resolves #7591 This moves our CI to build macOS on Sequoia (macOS 15) with Xcode 26, including the new macOS 26 beta SDK. Importantly, this will make our builds on macOS 26 use the new styling. I've added a new job that ensures we can continue to build with Xcode 16 and the macOS 15 SDK, as well, although I think that might come to an end when we switch over to an IconComposer-based icon. I'll verify then. For now, we continue to support both. I've also removed our `hasLiquidGlass` check, since this will now always be true for macOS 26 builds.
199 lines
7.8 KiB
Swift
199 lines
7.8 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?
|
|
|
|
deinit {
|
|
tabGroupWindowsObservation?.invalidate()
|
|
tabBarVisibleObservation?.invalidate()
|
|
}
|
|
|
|
// MARK: NSWindow
|
|
|
|
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()
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func update() {
|
|
super.update()
|
|
|
|
// On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our
|
|
// titlebar to be truly transparent.
|
|
if #unavailable(macOS 26) {
|
|
if !effectViewIsHidden {
|
|
hideEffectView()
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 }
|
|
|
|
// Setup the titlebar background color to match ours
|
|
titlebarContainer.wantsLayer = true
|
|
titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor
|
|
|
|
// See the docs for the function that sets this to true on why
|
|
effectViewIsHidden = false
|
|
|
|
// Necessary to not draw the border around the title
|
|
titlebarAppearsTransparent = true
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// MARK: macOS 13 to 15
|
|
|
|
// We only need to set this once, but need to do it after the window has been created in order
|
|
// to determine if the theme is using a very dark background, in which case we don't want to
|
|
// remove the effect view if the default tab bar is being used since the effect created in
|
|
// `updateTabsForVeryDarkBackgrounds` creates a confusing visual design.
|
|
private var effectViewIsHidden = false
|
|
|
|
private func hideEffectView() {
|
|
guard !effectViewIsHidden else { return }
|
|
|
|
// By hiding the visual effect view, we allow the window's (or titlebar's in this case)
|
|
// background color to show through. If we were to set `titlebarAppearsTransparent` to true
|
|
// the selected tab would look fine, but the unselected ones and new tab button backgrounds
|
|
// would be an opaque color. When the titlebar isn't transparent, however, the system applies
|
|
// a compositing effect to the unselected tab backgrounds, which makes them blend with the
|
|
// titlebar's/window's background.
|
|
if let effectView = titlebarContainer?.descendants(withClassName: "NSVisualEffectView").first {
|
|
effectView.isHidden = true
|
|
}
|
|
|
|
effectViewIsHidden = true
|
|
}
|
|
}
|