Files
wehub-resource-sync bd44b5aa08
Test / build-libghostty-vt (x86_64-windows-gnu) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-ios) (push) Has been cancelled
Test / build-libghostty-vt-macos (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt-macos (x86_64-macos) (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-dist (push) Has been cancelled
Test / GTK x11=true wayland=true (push) Has been cancelled
Test / Build -Dsimd=false (push) Has been cancelled
Test / Build -Dsimd=true (push) Has been cancelled
Test / Build -Dsentry=false (push) Has been cancelled
Test / Build -Dsentry=true (push) Has been cancelled
Test / zig-fmt (push) Has been cancelled
Test / GitHub Actions Pins (push) Has been cancelled
Test / prettier (push) Has been cancelled
Test / swiftlint (push) Has been cancelled
Test / alejandra (push) Has been cancelled
Test / typos (push) Has been cancelled
Nix / Required Checks: Nix (push) Has been cancelled
Nix / check-zig-cache-hash (push) Has been cancelled
Test / skip (push) Has been cancelled
Test / Required Checks: Test (push) Has been cancelled
Test / build-bench (push) Has been cancelled
Test / list-examples (push) Has been cancelled
Test / Example ${{ matrix.dir }} (Windows) (push) Has been cancelled
Test / build-cmake (push) Has been cancelled
Test / test-lib-vt-pkgconfig (push) Has been cancelled
Test / build-flatpak (push) Has been cancelled
Test / build-snap (push) Has been cancelled
Test / build-libghostty-vt (aarch64-linux) (push) Has been cancelled
Test / build-libghostty-vt (aarch64-macos) (push) Has been cancelled
Test / build-libghostty-vt (wasm32-freestanding) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-linux-musl) (push) Has been cancelled
Test / build-libghostty-vt (x86_64-macos) (push) Has been cancelled
Test / build-libghostty-vt-android (aarch64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-android (arm-linux-androideabi) (push) Has been cancelled
Test / build-libghostty-vt-android (x86_64-linux-android) (push) Has been cancelled
Test / build-libghostty-vt-windows (push) Has been cancelled
Test / build-libghostty-windows-gnu (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-linux (namespace-profile-ghostty-md-arm64) (push) Has been cancelled
Test / build-linux-libghostty (push) Has been cancelled
Test / build-nix (namespace-profile-ghostty-md) (push) Has been cancelled
Test / build-dist-lib-vt (push) Has been cancelled
Test / trigger-snap (push) Has been cancelled
Test / trigger-flatpak (push) Has been cancelled
Test / build-macos (push) Has been cancelled
Test / build-macos-freetype (push) Has been cancelled
Test / test (push) Has been cancelled
Test / test-lib-vt (push) Has been cancelled
Test / GTK x11=false wayland=false (push) Has been cancelled
Test / GTK x11=true wayland=false (push) Has been cancelled
Test / GTK x11=false wayland=true (push) Has been cancelled
Test / test-macos (push) Has been cancelled
Test / test-windows (push) Has been cancelled
Test / Build -Di18n=false (push) Has been cancelled
Test / Build -Di18n=true (push) Has been cancelled
Test / Build test/fuzz-libghostty (push) Has been cancelled
Test / shellcheck (push) Has been cancelled
Test / translations (push) Has been cancelled
Test / blueprint-compiler (push) Has been cancelled
Test / Test pkg/wuffs (push) Has been cancelled
Test / Test build on Debian 13 (push) Has been cancelled
Test / valgrind (push) Has been cancelled
Test / Example ${{ matrix.dir }} (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:48 +08:00

209 lines
8.3 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)
// override appearance based on the terminal's background color
if let preferredBackgroundColor {
appearance = (preferredBackgroundColor.isLightColor ? NSAppearance(named: .aqua) : NSAppearance(named: .darkAqua))
}
// Save our config in case we need to reapply
lastSurfaceConfig = surfaceConfig
// Every time 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.
//
// Even if we aren't transparent, we still set this because this becomes the
// color of the titlebar in native fullscreen view.
if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") {
titlebarView.wantsLayer = true
// For glass background styles, use a transparent titlebar to let the glass effect show through
// Only apply this for transparent and tabs titlebar styles
let isGlassStyle = derivedConfig.backgroundBlur.isGlassStyle
let isTransparentTitlebar = derivedConfig.macosTitlebarStyle == .transparent ||
derivedConfig.macosTitlebarStyle == .tabs
titlebarView.layer?.backgroundColor = (isGlassStyle && isTransparentTitlebar)
? NSColor.clear.cgColor
: 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] _, _ 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] _, _ 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
}
}