mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
@ -80,18 +80,6 @@ class TerminalWindow: NSWindow {
|
|||||||
|
|
||||||
_ = bindings
|
_ = bindings
|
||||||
|
|
||||||
// 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 titlebarContainer = contentView?.superview?.subviews.first(where: {
|
|
||||||
$0.className == "NSTitlebarContainerView"
|
|
||||||
}), let effectView = titlebarContainer.descendants(withClassName: "NSVisualEffectView").first {
|
|
||||||
effectView.isHidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the tab accessory view that houses the key-equivalent label and optional un-zoom button
|
// Create the tab accessory view that houses the key-equivalent label and optional un-zoom button
|
||||||
let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton])
|
let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton])
|
||||||
stackView.setHuggingPriority(.defaultHigh, for: .horizontal)
|
stackView.setHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
@ -131,25 +119,81 @@ class TerminalWindow: NSWindow {
|
|||||||
resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor
|
resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func layoutIfNeeded() {
|
||||||
|
super.layoutIfNeeded()
|
||||||
|
|
||||||
|
guard titlebarTabs else { return }
|
||||||
|
|
||||||
|
// We need to be aggressive with this, and it has to be done as well in `update`,
|
||||||
|
// otherwise things can get out of sync and flickering can occur.
|
||||||
|
updateTabsForVeryDarkBackgrounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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`.
|
||||||
|
private var effectViewIsHidden = false
|
||||||
|
|
||||||
override func update() {
|
override func update() {
|
||||||
super.update()
|
super.update()
|
||||||
|
|
||||||
updateResetZoomTitlebarButtonVisibility()
|
|
||||||
|
|
||||||
titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none
|
titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none
|
||||||
|
|
||||||
|
if !effectViewIsHidden {
|
||||||
|
// 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 titlebarContainer = contentView?.superview?.subviews.first(where: {
|
||||||
|
$0.className == "NSTitlebarContainerView"
|
||||||
|
}), let effectView = titlebarContainer.descendants(withClassName: "NSVisualEffectView").first {
|
||||||
|
effectView.isHidden = titlebarTabs || !titlebarTabs && !hasVeryDarkBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
effectViewIsHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if titlebarTabs {
|
||||||
|
updateTabsForVeryDarkBackgrounds()
|
||||||
// This is called when we open, close, switch, and reorder tabs, at which point we determine if the
|
// This is called when we open, close, switch, and reorder tabs, at which point we determine if the
|
||||||
// first tab in the tab bar is selected. If it is, we make the `windowButtonsBackdrop` color the same
|
// first tab in the tab bar is selected. If it is, we make the `windowButtonsBackdrop` color the same
|
||||||
// as that of the active tab (i.e. the titlebar's background color), otherwise we make it the same
|
// as that of the active tab (i.e. the titlebar's background color), otherwise we make it the same
|
||||||
// color as the background of unselected tabs.
|
// color as the background of unselected tabs.
|
||||||
if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self), titlebarTabs {
|
if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self) {
|
||||||
windowButtonsBackdrop?.isHighlighted = index == 0
|
windowButtonsBackdrop?.isHighlighted = index == 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNewTabButtonOpacity()
|
||||||
|
updateNewTabButtonImage()
|
||||||
|
updateResetZoomTitlebarButtonVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: -
|
||||||
|
|
||||||
|
private var newTabButtonImageLayer: VibrantLayer? = nil
|
||||||
|
|
||||||
|
// Since we are coloring the new tab button's image, it doesn't respond to the
|
||||||
|
// window's key status changes in terms of becoming less prominent visually,
|
||||||
|
// so we need to do it manually.
|
||||||
|
private func updateNewTabButtonOpacity() {
|
||||||
|
guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
|
||||||
|
$0.className == "NSTitlebarContainerView"
|
||||||
|
}) else { return }
|
||||||
|
guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
||||||
|
guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: {
|
||||||
|
$0 as? NSImageView != nil
|
||||||
|
}) as? NSImageView else { return }
|
||||||
|
|
||||||
|
newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5
|
||||||
|
}
|
||||||
|
|
||||||
// Color the new tab button's image to match the color of the tab title/keyboard shortcut labels,
|
// Color the new tab button's image to match the color of the tab title/keyboard shortcut labels,
|
||||||
// just as it does in the stock tab bar.
|
// just as it does in the stock tab bar.
|
||||||
updateNewTabButtonOpacity()
|
private func updateNewTabButtonImage() {
|
||||||
|
|
||||||
guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
|
guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
|
||||||
$0.className == "NSTitlebarContainerView"
|
$0.className == "NSTitlebarContainerView"
|
||||||
}) else { return }
|
}) else { return }
|
||||||
@ -186,23 +230,28 @@ class TerminalWindow: NSWindow {
|
|||||||
newTabButtonImageView.frame = newTabButton.bounds
|
newTabButtonImageView.frame = newTabButton.bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -
|
var hasVeryDarkBackground: Bool {
|
||||||
|
backgroundColor.luminance < 0.05
|
||||||
|
}
|
||||||
|
|
||||||
private var newTabButtonImageLayer: VibrantLayer? = nil
|
lazy var backgroundColorWithOpacity: NSColor = backgroundColor.withAlphaComponent(titlebarOpacity)
|
||||||
|
|
||||||
|
private func updateTabsForVeryDarkBackgrounds() {
|
||||||
|
guard hasVeryDarkBackground else { return }
|
||||||
|
|
||||||
// Since we are coloring the new tab button's image, it doesn't respond to the
|
|
||||||
// window's key status changes in terms of becoming less prominent visually,
|
|
||||||
// so we need to do it manually.
|
|
||||||
private func updateNewTabButtonOpacity() {
|
|
||||||
guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
|
guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
|
||||||
$0.className == "NSTitlebarContainerView"
|
$0.className == "NSTitlebarContainerView"
|
||||||
}) else { return }
|
}) else { return }
|
||||||
guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
|
||||||
guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: {
|
|
||||||
$0 as? NSImageView != nil
|
|
||||||
}) as? NSImageView else { return }
|
|
||||||
|
|
||||||
newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5
|
if let tabGroup = tabGroup, tabGroup.isTabBarVisible {
|
||||||
|
guard let activeTabBackgroundView = titlebarContainer.firstDescendant(withClassName: "NSTabButton")?.superview?.subviews.last?.firstDescendant(withID: "_backgroundView")
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
activeTabBackgroundView.layer?.backgroundColor = backgroundColorWithOpacity.cgColor
|
||||||
|
titlebarContainer.layer?.backgroundColor = backgroundColorWithOpacity.highlight(withLevel: 0.14)?.cgColor
|
||||||
|
} else {
|
||||||
|
titlebarContainer.layer?.backgroundColor = backgroundColorWithOpacity.cgColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateResetZoomTitlebarButtonVisibility() {
|
private func updateResetZoomTitlebarButtonVisibility() {
|
||||||
@ -407,9 +456,7 @@ class TerminalWindow: NSWindow {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let backdropColor = backgroundColor.withAlphaComponent(titlebarOpacity).usingColorSpace(colorSpace!)!.cgColor
|
let view = WindowButtonsBackdropView(window: self)
|
||||||
|
|
||||||
let view = WindowButtonsBackdropView(backgroundColor: backdropColor)
|
|
||||||
view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop")
|
view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop")
|
||||||
titlebarView.addSubview(view)
|
titlebarView.addSubview(view)
|
||||||
|
|
||||||
@ -491,8 +538,9 @@ fileprivate class WindowDragView: NSView {
|
|||||||
|
|
||||||
// A view that matches the color of selected and unselected tabs in the adjacent tab bar.
|
// A view that matches the color of selected and unselected tabs in the adjacent tab bar.
|
||||||
fileprivate class WindowButtonsBackdropView: NSView {
|
fileprivate class WindowButtonsBackdropView: NSView {
|
||||||
private let overlayLayer = VibrantLayer()
|
private let terminalWindow: TerminalWindow
|
||||||
private let isLightTheme: Bool
|
private let isLightTheme: Bool
|
||||||
|
private let overlayLayer = VibrantLayer()
|
||||||
|
|
||||||
var isHighlighted: Bool = true {
|
var isHighlighted: Bool = true {
|
||||||
didSet {
|
didSet {
|
||||||
@ -500,8 +548,14 @@ fileprivate class WindowButtonsBackdropView: NSView {
|
|||||||
overlayLayer.isHidden = isHighlighted
|
overlayLayer.isHidden = isHighlighted
|
||||||
layer?.backgroundColor = .clear
|
layer?.backgroundColor = .clear
|
||||||
} else {
|
} else {
|
||||||
|
let systemOverlayColor = NSColor(cgColor: CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.45))!
|
||||||
|
let titlebarBackgroundColor = terminalWindow.backgroundColorWithOpacity.blended(withFraction: 1, of: systemOverlayColor)
|
||||||
|
|
||||||
|
let highlightedColor = terminalWindow.hasVeryDarkBackground ? terminalWindow.backgroundColor : .clear
|
||||||
|
let backgroundColor = terminalWindow.hasVeryDarkBackground ? titlebarBackgroundColor : systemOverlayColor
|
||||||
|
|
||||||
overlayLayer.isHidden = true
|
overlayLayer.isHidden = true
|
||||||
layer?.backgroundColor = isHighlighted ? .clear : CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.45)
|
layer?.backgroundColor = isHighlighted ? highlightedColor?.cgColor : backgroundColor?.cgColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -510,8 +564,9 @@ fileprivate class WindowButtonsBackdropView: NSView {
|
|||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
init(backgroundColor: CGColor) {
|
init(window: TerminalWindow) {
|
||||||
self.isLightTheme = NSColor(cgColor: backgroundColor)!.isLightColor
|
self.terminalWindow = window
|
||||||
|
self.isLightTheme = window.backgroundColor.isLightColor
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
@ -28,4 +28,17 @@ extension NSView {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively finds and returns the first descendant view that has the given identifier.
|
||||||
|
func firstDescendant(withID id: String) -> NSView? {
|
||||||
|
for subview in subviews {
|
||||||
|
if subview.identifier == NSUserInterfaceItemIdentifier(id) {
|
||||||
|
return subview
|
||||||
|
} else if let found = subview.firstDescendant(withID: id) {
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user