Merge pull request #1550 from peteschaffner/titlebar-unzoom-button

Add un-zoom button to titlebar and tabs
This commit is contained in:
Mitchell Hashimoto
2024-03-26 09:41:53 -07:00
committed by GitHub
6 changed files with 346 additions and 217 deletions

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "ResetZoom.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

View File

@ -121,7 +121,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// Reset this to false. It'll be set back to true later.
tabListenForFrame = false
guard let windows = self.window?.tabbedWindows else { return }
guard let windows = self.window?.tabbedWindows as? [TerminalWindow] else { return }
// We only listen for frame changes if we have more than 1 window,
// otherwise the accessory view doesn't matter.
@ -129,19 +129,10 @@ class TerminalController: NSWindowController, NSWindowDelegate,
for (index, window) in windows.enumerated().prefix(9) {
let action = "goto_tab:\(index + 1)"
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
continue
}
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.labelFont(ofSize: 0),
.foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor,
]
let attributedString = NSAttributedString(string: " \(equiv) ", attributes: attributes)
let text = NSTextField(labelWithAttributedString: attributedString)
text.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal)
text.postsFrameChangedNotifications = true
window.tab.accessoryView = text
if let equiv = ghostty.config.keyEquivalent(for: action) {
window.keyEquivalent = "\(equiv)"
}
}
}
@ -255,6 +246,12 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// when cascading.
window.center()
// Set the background color of the window
window.backgroundColor = NSColor(ghostty.config.backgroundColor)
// This makes sure our titlebar renders correctly when there is a transparent background
window.titlebarOpacity = ghostty.config.backgroundOpacity
// Handle titlebar tabs config option. Something about what we do while setting up the
// titlebar tabs interferes with the window restore process unless window.tabbingMode
// is set to .preferred, so we set it, and switch back to automatic as soon as we can.
@ -265,18 +262,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
DispatchQueue.main.async {
window.tabbingMode = .automatic
}
// Set the background color of the window
window.backgroundColor = NSColor(ghostty.config.backgroundColor)
// Set a custom background on the titlebar - this is required for when
// titlebar tabs are used in conjunction with a transparent background.
window.setTitlebarBackground(
window
.backgroundColor
.withAlphaComponent(ghostty.config.backgroundOpacity)
.cgColor
)
}
// Initialize our content view to the SwiftUI root
@ -595,6 +580,11 @@ class TerminalController: NSWindowController, NSWindowDelegate,
invalidateRestorableState()
}
func zoomStateDidChange(to: Bool) {
guard let window = window as? TerminalWindow else { return }
window.surfaceIsZoomed = to
}
//MARK: - Clipboard Confirmation
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {

View File

@ -3,7 +3,6 @@ import Cocoa
// Custom NSToolbar subclass that displays a centered window title,
// in order to accommodate the titlebar tabs feature.
class TerminalToolbar: NSToolbar, NSToolbarDelegate {
static private let identifier = NSToolbarItem.Identifier("TitleText")
private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty")
var titleText: String {
@ -22,22 +21,22 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate {
delegate = self
if #available(macOS 13.0, *) {
centeredItemIdentifiers.insert(Self.identifier)
centeredItemIdentifiers.insert(.titleText)
} else {
centeredItemIdentifier = Self.identifier
centeredItemIdentifier = .titleText
}
}
func toolbar(_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
guard itemIdentifier == Self.identifier else {
return NSToolbarItem(itemIdentifier: itemIdentifier)
}
var item: NSToolbarItem
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.view = self.titleTextField
toolbarItem.visibilityPriority = .user
switch itemIdentifier {
case .titleText:
item = NSToolbarItem(itemIdentifier: .titleText)
item.view = self.titleTextField
item.visibilityPriority = .user
// NSToolbarItem.minSize and NSToolbarItem.maxSize are deprecated, and make big ugly
// warnings in Xcode when you use them, but I cannot for the life of me figure out
@ -47,24 +46,29 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate {
// whether I have constraints set on its width, height, or both :/
//
// If someone can fix this so we don't have to use deprecated properties: Please do.
toolbarItem.minSize = NSSize(width: 32, height: 1)
toolbarItem.maxSize = NSSize(width: 1024, height: self.titleTextField.intrinsicContentSize.height)
item.minSize = NSSize(width: 32, height: 1)
item.maxSize = NSSize(width: 1024, height: self.titleTextField.intrinsicContentSize.height)
toolbarItem.isEnabled = true
item.isEnabled = true
case .resetZoom:
item = NSToolbarItem(itemIdentifier: .resetZoom)
default:
item = NSToolbarItem(itemIdentifier: itemIdentifier)
}
return toolbarItem
return item
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [Self.identifier, .space]
return [.titleText, .flexibleSpace, .space, .resetZoom]
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
// These space items are here to ensure that the title remains centered when it starts
// getting smaller than the max size so starts clipping. Lucky for us, three of the
// built-in spacers seems to exactly match the space on the left that's reserved for
// the window buttons.
return [Self.identifier, .space, .space, .space]
// getting smaller than the max size so starts clipping. Lucky for us, two of the
// built-in spacers plus the un-zoom button item seems to exactly match the space
// on the left that's reserved for the window buttons.
return [.titleText, .flexibleSpace, .space, .space, .resetZoom]
}
}
@ -83,3 +87,8 @@ fileprivate class CenteredDynamicLabel: NSTextField {
needsLayout = true
}
}
extension NSToolbarItem.Identifier {
static let resetZoom = NSToolbarItem.Identifier("ResetZoom")
static let titleText = NSToolbarItem.Identifier("TitleText")
}

View File

@ -17,6 +17,8 @@ protocol TerminalViewDelegate: AnyObject {
/// The surface tree did change in some way, i.e. a split was added, removed, etc. This is
/// not called initially.
func surfaceTreeDidChange()
func zoomStateDidChange(to: Bool)
}
// Default all the functions so they're optional
@ -24,6 +26,7 @@ extension TerminalViewDelegate {
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {}
func titleDidChange(to: String) {}
func cellSizeDidChange(to: NSSize) {}
func zoomStateDidChange(to: Bool) {}
}
/// The view model is a required implementation for TerminalView callers. This contains
@ -64,12 +67,6 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
}
}
if let zoomedSplit = zoomedSplit {
if zoomedSplit {
title = "🔍 " + title
}
}
return title
}
@ -107,6 +104,9 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
// in the hash value.
self.delegate?.surfaceTreeDidChange()
}
.onChange(of: zoomedSplit) { newValue in
self.delegate?.zoomStateDidChange(to: newValue ?? false)
}
}
}
}

View File

@ -1,15 +1,116 @@
import Cocoa
class TerminalWindow: NSWindow {
@objc dynamic var surfaceIsZoomed: Bool = false
@objc dynamic var keyEquivalent: String = ""
var titlebarOpacity: CGFloat = 1 {
didSet {
guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
$0.className == "NSTitlebarContainerView"
}) else { return }
titlebarContainer.wantsLayer = true
titlebarContainer.layer?.backgroundColor = backgroundColor.withAlphaComponent(titlebarOpacity).cgColor
}
}
private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton()
private lazy var resetZoomTabButton: NSButton = {
let button = generateResetZoomButton()
button.action = #selector(selectTabAndZoom(_:))
return button
}()
private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = {
guard let titlebarContainer = contentView?.superview?.subviews.first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil }
let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height)
let view = NSView(frame: NSRect(origin: .zero, size: size))
let button = generateResetZoomButton()
button.frame.origin.x = size.width/2 - button.bounds.width/2
button.frame.origin.y = size.height/2 - button.bounds.height/2
view.addSubview(button)
let titlebarAccessoryViewController = NSTitlebarAccessoryViewController()
titlebarAccessoryViewController.view = view
titlebarAccessoryViewController.layoutAttribute = .right
return titlebarAccessoryViewController
}()
private lazy var keyEquivalentLabel: NSTextField = {
let label = NSTextField(labelWithAttributedString: NSAttributedString())
label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal)
label.postsFrameChangedNotifications = true
return label
}()
private lazy var bindings = [
observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in
guard let tabGroup = self?.tabGroup else { return }
self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed
self?.updateResetZoomTitlebarButtonVisibility()
},
observe(\.keyEquivalent, options: [.initial, .new]) { [weak self] window, _ in
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
.foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor,
]
let attributedString = NSAttributedString(string: " \(window.keyEquivalent) ", attributes: attributes)
self?.keyEquivalentLabel.attributedStringValue = attributedString
},
]
// Both of these must be true for windows without decorations to be able to
// still become key/main and receive events.
override var canBecomeKey: Bool { return true }
override var canBecomeMain: Bool { return true }
// MARK: - Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
_ = 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
let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton])
stackView.setHuggingPriority(.defaultHigh, for: .horizontal)
stackView.spacing = 3
tab.accessoryView = stackView
if titlebarTabs {
generateToolbar()
}
}
deinit {
bindings.forEach() { $0.invalidate() }
}
// MARK: - NSWindow
override func becomeKey() {
// This is required because the removeTitlebarAccessoryViewControlle hook does not
// This is required because the removeTitlebarAccessoryViewController hook does not
// catch the creation of a new window by "tearing off" a tab from a tabbed window.
if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 {
hideCustomTabBarViews()
@ -17,17 +118,161 @@ class TerminalWindow: NSWindow {
super.becomeKey()
if titlebarTabs {
updateNewTabButtonOpacity()
}
resetZoomTabButton.contentTintColor = .controlAccentColor
resetZoomToolbarButton.contentTintColor = .controlAccentColor
}
override func resignKey() {
super.resignKey()
if titlebarTabs {
updateNewTabButtonOpacity()
resetZoomTabButton.contentTintColor = .secondaryLabelColor
resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor
}
override func update() {
super.update()
updateResetZoomTitlebarButtonVisibility()
titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none
// 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
// 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.
if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self), titlebarTabs {
windowButtonsBackdrop?.isHighlighted = index == 0
}
// 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.
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 }
guard let newTabButtonImage = newTabButtonImageView.image else { return }
let isLightTheme = backgroundColor.isLightColor
if newTabButtonImageLayer == nil {
let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85)
let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in
newTabButtonImage.draw(in: rect)
fillColor.setFill()
rect.fill(using: .sourceAtop)
return true
}
let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)!
imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size)
imageLayer.contentsGravity = .resizeAspect
imageLayer.contents = newImage
imageLayer.opacity = 0.5
newTabButtonImageLayer = imageLayer
}
newTabButtonImageView.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer()
newTabButtonImageView.layer?.addSublayer(newTabButtonImageLayer!)
newTabButtonImageView.image = nil
// When we nil out the original image, the image view's frame resizes and repositions
// slightly, so we need to reset it to make sure our new image doesn't shift quickly.
newTabButtonImageView.frame = newTabButton.bounds
}
// 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
}
private func updateResetZoomTitlebarButtonVisibility() {
guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return }
let isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed
if titlebarTabs {
resetZoomToolbarButton.isHidden = isHidden
for (index, vc) in titlebarAccessoryViewControllers.enumerated() {
guard vc == resetZoomTitlebarAccessoryViewController else { return }
removeTitlebarAccessoryViewController(at: index)
}
} else {
if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) {
addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController)
}
resetZoomTitlebarAccessoryViewController.view.isHidden = isHidden
}
}
// We have to regenerate a toolbar when the titlebar tabs setting changes since our
// custom toolbar conditionally generates the items based on this setting. I tried to
// invalidate the toolbar items and force a refresh, but as far as I can tell that
// isn't possible.
private func generateToolbar() {
let terminalToolbar = TerminalToolbar(identifier: "Toolbar")
toolbar = terminalToolbar
toolbarStyle = .unifiedCompact
if let resetZoomItem = terminalToolbar.items.first(where: { $0.itemIdentifier == .resetZoom }) {
resetZoomItem.view = resetZoomToolbarButton
resetZoomItem.view?.translatesAutoresizingMaskIntoConstraints = false
resetZoomItem.view?.widthAnchor.constraint(equalToConstant: 22).isActive = true
resetZoomItem.view?.heightAnchor.constraint(equalToConstant: 20).isActive = true
}
updateResetZoomTitlebarButtonVisibility()
}
private func generateResetZoomButton() -> NSButton {
let button = NSButton()
button.target = nil
button.action = #selector(TerminalController.splitZoom(_:))
button.isBordered = false
button.allowsExpansionToolTips = true
button.toolTip = "Reset Zoom"
button.contentTintColor = .controlAccentColor
button.state = .on
button.image = NSImage(named:"ResetZoom")
button.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalToConstant: 20).isActive = true
button.heightAnchor.constraint(equalToConstant: 20).isActive = true
return button
}
@objc private func selectTabAndZoom(_ sender: NSButton) {
guard let tabGroup else { return }
guard let associatedWindow = tabGroup.windows.first(where: {
guard let accessoryView = $0.tab.accessoryView else { return false }
return accessoryView.subviews.contains(sender)
}),
let windowController = associatedWindow.windowController as? TerminalController
else { return }
tabGroup.selectedWindow = associatedWindow
windowController.splitZoom(self)
}
// MARK: - Titlebar Tabs
@ -35,14 +280,15 @@ class TerminalWindow: NSWindow {
// Used by the window controller to enable/disable titlebar tabs.
var titlebarTabs = false {
didSet {
changedTitlebarTabs(to: titlebarTabs)
self.titleVisibility = titlebarTabs ? .hidden : .visible
if titlebarTabs {
generateToolbar()
}
}
}
private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil
private var windowDragHandle: WindowDragView? = nil
private var storedTitlebarBackgroundColor: CGColor? = nil
private var newTabButtonImageLayer: VibrantLayer? = nil
// The tab bar controller ID from macOS
static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController")
@ -66,72 +312,6 @@ class TerminalWindow: NSWindow {
}
}
/// This is called by titlebarTabs changing so that we can setup the rest of our window
private func changedTitlebarTabs(to newValue: Bool) {
if (newValue) {
// 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
}
self.titlebarSeparatorStyle = .none
// We use the toolbar to anchor our tab bar positions in the titlebar,
// so we make sure it's the right size/position, and exists.
self.toolbarStyle = .unifiedCompact
if (self.toolbar == nil) {
self.toolbar = TerminalToolbar(identifier: "Toolbar")
}
// Set a custom background on the titlebar - this is required for when
// titlebar tabs is used in conjunction with a transparent background.
self.restoreTitlebarBackground()
// Reset the new tab button image so that we are sure to generate a fresh
// one, tinted appropriately for the given theme.
self.newTabButtonImageLayer = nil
// We have to wait before setting the titleVisibility or else it prevents
// the window from hiding the tab bar when we get down to a single tab.
DispatchQueue.main.async {
self.titleVisibility = .hidden
}
} else {
// "expanded" places the toolbar below the titlebar, so setting this style and
// removing the toolbar ensures that the titlebar will be the default height.
self.toolbarStyle = .expanded
self.toolbar = nil
// Reset the appearance to whatever our app global value is
self.appearance = nil
}
}
// Assign a background color to the titlebar area.
func setTitlebarBackground(_ color: CGColor) {
storedTitlebarBackgroundColor = color
guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
$0.className == "NSTitlebarContainerView"
}) else { return }
titlebarContainer.wantsLayer = true
titlebarContainer.layer?.backgroundColor = color
}
// Make sure the titlebar has the assigned background color.
private func restoreTitlebarBackground() {
guard let color = storedTitlebarBackgroundColor else { return }
setTitlebarBackground(color)
}
// This is called by macOS for native tabbing in order to add the tab bar. We hook into
// this, detect the tab bar being added, and override its behavior.
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
@ -214,73 +394,6 @@ class TerminalWindow: NSWindow {
}
}
override func update() {
super.update()
guard titlebarTabs else { return }
// 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
// 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.
if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self) {
windowButtonsBackdrop?.isHighlighted = index == 0
}
// 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.
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 }
guard let newTabButtonImage = newTabButtonImageView.image else { return }
guard let storedTitlebarBackgroundColor, let isLightTheme = NSColor(cgColor: storedTitlebarBackgroundColor)?.isLightColor else { return }
if newTabButtonImageLayer == nil {
let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85)
let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in
newTabButtonImage.draw(in: rect)
fillColor.setFill()
rect.fill(using: .sourceAtop)
return true
}
let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)!
imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size)
imageLayer.contentsGravity = .resizeAspect
imageLayer.contents = newImage
imageLayer.opacity = 0.5
newTabButtonImageLayer = imageLayer
}
newTabButtonImageView.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer()
newTabButtonImageView.layer?.addSublayer(newTabButtonImageLayer!)
newTabButtonImageView.image = nil
// When we nil out the original image, the image view's frame resizes and repositions
// slightly, so we need to reset it to make sure our new image doesn't shift quickly.
newTabButtonImageView.frame = newTabButton.bounds
}
// 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
}
private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) {
// If we already made the view, just make sure it's unhidden and correctly placed as a subview.
if let view = windowButtonsBackdrop {
@ -294,7 +407,9 @@ class TerminalWindow: NSWindow {
return
}
let view = WindowButtonsBackdropView(backgroundColor: storedTitlebarBackgroundColor ?? NSColor.windowBackgroundColor.cgColor)
let backdropColor = backgroundColor.withAlphaComponent(titlebarOpacity).usingColorSpace(colorSpace!)!.cgColor
let view = WindowButtonsBackdropView(backgroundColor: backdropColor)
view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop")
titlebarView.addSubview(view)