mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Revert "Merge pull request #1550 from peteschaffner/titlebar-unzoom-button"
This reverts commit 6b7a1ce1eb364936b58408bd67bbeaf0d3dc8579, reversing changes made to b68e1c6a5d17b7e7b35902873a3241ee663f49a3.
This commit is contained in:
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"filename" : "ResetZoom.pdf",
|
|
||||||
"idiom" : "universal"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
},
|
|
||||||
"properties" : {
|
|
||||||
"template-rendering-intent" : "template"
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
@ -54,8 +54,8 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
/// This is set to false by init if the window managed by this controller should not be restorable.
|
/// This is set to false by init if the window managed by this controller should not be restorable.
|
||||||
/// For example, terminals executing custom scripts are not restorable.
|
/// For example, terminals executing custom scripts are not restorable.
|
||||||
private var restorable: Bool = true
|
private var restorable: Bool = true
|
||||||
|
|
||||||
init(_ ghostty: Ghostty.App,
|
init(_ ghostty: Ghostty.App,
|
||||||
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
withBaseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||||
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
withSurfaceTree tree: Ghostty.SplitNode? = nil
|
||||||
) {
|
) {
|
||||||
@ -120,22 +120,31 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
func relabelTabs() {
|
func relabelTabs() {
|
||||||
// Reset this to false. It'll be set back to true later.
|
// Reset this to false. It'll be set back to true later.
|
||||||
tabListenForFrame = false
|
tabListenForFrame = false
|
||||||
|
|
||||||
guard let windows = self.window?.tabbedWindows as? [TerminalWindow] else { return }
|
guard let windows = self.window?.tabbedWindows else { return }
|
||||||
|
|
||||||
// We only listen for frame changes if we have more than 1 window,
|
// We only listen for frame changes if we have more than 1 window,
|
||||||
// otherwise the accessory view doesn't matter.
|
// otherwise the accessory view doesn't matter.
|
||||||
tabListenForFrame = windows.count > 1
|
tabListenForFrame = windows.count > 1
|
||||||
|
|
||||||
for (index, window) in windows.enumerated().prefix(9) {
|
for (index, window) in windows.enumerated().prefix(9) {
|
||||||
let action = "goto_tab:\(index + 1)"
|
let action = "goto_tab:\(index + 1)"
|
||||||
|
guard let equiv = ghostty.config.keyEquivalent(for: action) else {
|
||||||
if let equiv = ghostty.config.keyEquivalent(for: action) {
|
continue
|
||||||
window.keyEquivalent = "\(equiv)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fixTabBar() {
|
private func fixTabBar() {
|
||||||
// We do this to make sure that the tab bar will always re-composite. If we don't,
|
// We do this to make sure that the tab bar will always re-composite. If we don't,
|
||||||
// then the it will "drag" pieces of the background with it when a transparent
|
// then the it will "drag" pieces of the background with it when a transparent
|
||||||
@ -191,7 +200,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
leaf.surface.focusDidChange(focused)
|
leaf.surface.focusDidChange(focused)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - NSWindowController
|
//MARK: - NSWindowController
|
||||||
|
|
||||||
override func windowWillLoad() {
|
override func windowWillLoad() {
|
||||||
@ -246,16 +255,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
// when cascading.
|
// when cascading.
|
||||||
window.center()
|
window.center()
|
||||||
|
|
||||||
// Set the background color of the window. We only do this if the lum is
|
|
||||||
// over 0.1 to prevent: https://github.com/mitchellh/ghostty/issues/1549
|
|
||||||
let bgColor = NSColor(ghostty.config.backgroundColor)
|
|
||||||
if (bgColor.luminance > 0.1) {
|
|
||||||
window.backgroundColor = bgColor
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// 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
|
// 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.
|
// is set to .preferred, so we set it, and switch back to automatic as soon as we can.
|
||||||
@ -266,8 +265,20 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
window.tabbingMode = .automatic
|
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
|
// Initialize our content view to the SwiftUI root
|
||||||
window.contentView = NSHostingView(rootView: TerminalView(
|
window.contentView = NSHostingView(rootView: TerminalView(
|
||||||
ghostty: self.ghostty,
|
ghostty: self.ghostty,
|
||||||
@ -302,7 +313,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
guard let surface = self.focusedSurface?.surface else { return }
|
guard let surface = self.focusedSurface?.surface else { return }
|
||||||
ghostty.newTab(surface: surface)
|
ghostty.newTab(surface: surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - NSWindowDelegate
|
//MARK: - NSWindowDelegate
|
||||||
|
|
||||||
// This is called when performClose is called on a window (NOT when close()
|
// This is called when performClose is called on a window (NOT when close()
|
||||||
@ -382,7 +393,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when the window will be encoded. We handle the data encoding here in the
|
// Called when the window will be encoded. We handle the data encoding here in the
|
||||||
// window controller.
|
// window controller.
|
||||||
func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) {
|
||||||
@ -583,12 +594,7 @@ class TerminalController: NSWindowController, NSWindowDelegate,
|
|||||||
// we want to invalidate our state.
|
// we want to invalidate our state.
|
||||||
invalidateRestorableState()
|
invalidateRestorableState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func zoomStateDidChange(to: Bool) {
|
|
||||||
guard let window = window as? TerminalWindow else { return }
|
|
||||||
window.surfaceIsZoomed = to
|
|
||||||
}
|
|
||||||
|
|
||||||
//MARK: - Clipboard Confirmation
|
//MARK: - Clipboard Confirmation
|
||||||
|
|
||||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
|
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) {
|
||||||
|
@ -3,6 +3,7 @@ import Cocoa
|
|||||||
// Custom NSToolbar subclass that displays a centered window title,
|
// Custom NSToolbar subclass that displays a centered window title,
|
||||||
// in order to accommodate the titlebar tabs feature.
|
// in order to accommodate the titlebar tabs feature.
|
||||||
class TerminalToolbar: NSToolbar, NSToolbarDelegate {
|
class TerminalToolbar: NSToolbar, NSToolbarDelegate {
|
||||||
|
static private let identifier = NSToolbarItem.Identifier("TitleText")
|
||||||
private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty")
|
private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty")
|
||||||
|
|
||||||
var titleText: String {
|
var titleText: String {
|
||||||
@ -14,61 +15,56 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate {
|
|||||||
titleTextField.stringValue = newValue
|
titleTextField.stringValue = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(identifier: NSToolbar.Identifier) {
|
override init(identifier: NSToolbar.Identifier) {
|
||||||
super.init(identifier: identifier)
|
super.init(identifier: identifier)
|
||||||
|
|
||||||
delegate = self
|
delegate = self
|
||||||
|
|
||||||
if #available(macOS 13.0, *) {
|
if #available(macOS 13.0, *) {
|
||||||
centeredItemIdentifiers.insert(.titleText)
|
centeredItemIdentifiers.insert(Self.identifier)
|
||||||
} else {
|
} else {
|
||||||
centeredItemIdentifier = .titleText
|
centeredItemIdentifier = Self.identifier
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolbar(_ toolbar: NSToolbar,
|
func toolbar(_ toolbar: NSToolbar,
|
||||||
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
|
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
|
||||||
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
|
willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
|
||||||
var item: NSToolbarItem
|
guard itemIdentifier == Self.identifier else {
|
||||||
|
return NSToolbarItem(itemIdentifier: itemIdentifier)
|
||||||
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
|
|
||||||
// how to get this to work with constraints. The behavior isn't the same, instead of
|
|
||||||
// shrinking the item and clipping the subview, it hides the item as soon as the
|
|
||||||
// intrinsic size of the subview gets too big for the toolbar width, regardless of
|
|
||||||
// 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.
|
|
||||||
item.minSize = NSSize(width: 32, height: 1)
|
|
||||||
item.maxSize = NSSize(width: 1024, height: self.titleTextField.intrinsicContentSize.height)
|
|
||||||
|
|
||||||
item.isEnabled = true
|
|
||||||
case .resetZoom:
|
|
||||||
item = NSToolbarItem(itemIdentifier: .resetZoom)
|
|
||||||
default:
|
|
||||||
item = NSToolbarItem(itemIdentifier: itemIdentifier)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return item
|
let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
|
||||||
|
toolbarItem.view = self.titleTextField
|
||||||
|
toolbarItem.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
|
||||||
|
// how to get this to work with constraints. The behavior isn't the same, instead of
|
||||||
|
// shrinking the item and clipping the subview, it hides the item as soon as the
|
||||||
|
// intrinsic size of the subview gets too big for the toolbar width, regardless of
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
toolbarItem.isEnabled = true
|
||||||
|
|
||||||
|
return toolbarItem
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||||
return [.titleText, .flexibleSpace, .space, .resetZoom]
|
return [Self.identifier, .space]
|
||||||
}
|
}
|
||||||
|
|
||||||
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||||
// These space items are here to ensure that the title remains centered when it starts
|
// 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, two of the
|
// getting smaller than the max size so starts clipping. Lucky for us, three of the
|
||||||
// built-in spacers plus the un-zoom button item seems to exactly match the space
|
// built-in spacers seems to exactly match the space on the left that's reserved for
|
||||||
// on the left that's reserved for the window buttons.
|
// the window buttons.
|
||||||
return [.titleText, .flexibleSpace, .space, .space, .resetZoom]
|
return [Self.identifier, .space, .space, .space]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,8 +83,3 @@ fileprivate class CenteredDynamicLabel: NSTextField {
|
|||||||
needsLayout = true
|
needsLayout = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NSToolbarItem.Identifier {
|
|
||||||
static let resetZoom = NSToolbarItem.Identifier("ResetZoom")
|
|
||||||
static let titleText = NSToolbarItem.Identifier("TitleText")
|
|
||||||
}
|
|
||||||
|
@ -17,8 +17,6 @@ protocol TerminalViewDelegate: AnyObject {
|
|||||||
/// The surface tree did change in some way, i.e. a split was added, removed, etc. This is
|
/// The surface tree did change in some way, i.e. a split was added, removed, etc. This is
|
||||||
/// not called initially.
|
/// not called initially.
|
||||||
func surfaceTreeDidChange()
|
func surfaceTreeDidChange()
|
||||||
|
|
||||||
func zoomStateDidChange(to: Bool)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default all the functions so they're optional
|
// Default all the functions so they're optional
|
||||||
@ -26,7 +24,6 @@ extension TerminalViewDelegate {
|
|||||||
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {}
|
func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {}
|
||||||
func titleDidChange(to: String) {}
|
func titleDidChange(to: String) {}
|
||||||
func cellSizeDidChange(to: NSSize) {}
|
func cellSizeDidChange(to: NSSize) {}
|
||||||
func zoomStateDidChange(to: Bool) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The view model is a required implementation for TerminalView callers. This contains
|
/// The view model is a required implementation for TerminalView callers. This contains
|
||||||
@ -67,6 +64,12 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let zoomedSplit = zoomedSplit {
|
||||||
|
if zoomedSplit {
|
||||||
|
title = "🔍 " + title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,9 +107,6 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
|||||||
// in the hash value.
|
// in the hash value.
|
||||||
self.delegate?.surfaceTreeDidChange()
|
self.delegate?.surfaceTreeDidChange()
|
||||||
}
|
}
|
||||||
.onChange(of: zoomedSplit) { newValue in
|
|
||||||
self.delegate?.zoomStateDidChange(to: newValue ?? false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,116 +1,15 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class TerminalWindow: NSWindow {
|
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
|
// Both of these must be true for windows without decorations to be able to
|
||||||
// still become key/main and receive events.
|
// still become key/main and receive events.
|
||||||
override var canBecomeKey: Bool { return true }
|
override var canBecomeKey: Bool { return true }
|
||||||
override var canBecomeMain: 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
|
// MARK: - NSWindow
|
||||||
|
|
||||||
override func becomeKey() {
|
override func becomeKey() {
|
||||||
// This is required because the removeTitlebarAccessoryViewController hook does not
|
// This is required because the removeTitlebarAccessoryViewControlle hook does not
|
||||||
// catch the creation of a new window by "tearing off" a tab from a tabbed window.
|
// 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 {
|
if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 {
|
||||||
hideCustomTabBarViews()
|
hideCustomTabBarViews()
|
||||||
@ -118,177 +17,32 @@ class TerminalWindow: NSWindow {
|
|||||||
|
|
||||||
super.becomeKey()
|
super.becomeKey()
|
||||||
|
|
||||||
updateNewTabButtonOpacity()
|
if titlebarTabs {
|
||||||
resetZoomTabButton.contentTintColor = .controlAccentColor
|
updateNewTabButtonOpacity()
|
||||||
resetZoomToolbarButton.contentTintColor = .controlAccentColor
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func resignKey() {
|
override func resignKey() {
|
||||||
super.resignKey()
|
super.resignKey()
|
||||||
|
|
||||||
updateNewTabButtonOpacity()
|
if titlebarTabs {
|
||||||
resetZoomTabButton.contentTintColor = .secondaryLabelColor
|
updateNewTabButtonOpacity()
|
||||||
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
|
// MARK: - Titlebar Tabs
|
||||||
|
|
||||||
// Used by the window controller to enable/disable titlebar tabs.
|
// Used by the window controller to enable/disable titlebar tabs.
|
||||||
var titlebarTabs = false {
|
var titlebarTabs = false {
|
||||||
didSet {
|
didSet {
|
||||||
self.titleVisibility = titlebarTabs ? .hidden : .visible
|
changedTitlebarTabs(to: titlebarTabs)
|
||||||
if titlebarTabs {
|
|
||||||
generateToolbar()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil
|
private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil
|
||||||
private var windowDragHandle: WindowDragView? = nil
|
private var windowDragHandle: WindowDragView? = nil
|
||||||
|
private var storedTitlebarBackgroundColor: CGColor? = nil
|
||||||
|
private var newTabButtonImageLayer: VibrantLayer? = nil
|
||||||
|
|
||||||
// The tab bar controller ID from macOS
|
// The tab bar controller ID from macOS
|
||||||
static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController")
|
static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController")
|
||||||
@ -312,6 +66,72 @@ 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 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.
|
// this, detect the tab bar being added, and override its behavior.
|
||||||
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
||||||
@ -394,6 +214,73 @@ 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) {
|
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 we already made the view, just make sure it's unhidden and correctly placed as a subview.
|
||||||
if let view = windowButtonsBackdrop {
|
if let view = windowButtonsBackdrop {
|
||||||
@ -407,9 +294,7 @@ class TerminalWindow: NSWindow {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let backdropColor = backgroundColor.withAlphaComponent(titlebarOpacity).usingColorSpace(colorSpace!)!.cgColor
|
let view = WindowButtonsBackdropView(backgroundColor: storedTitlebarBackgroundColor ?? NSColor.windowBackgroundColor.cgColor)
|
||||||
|
|
||||||
let view = WindowButtonsBackdropView(backgroundColor: backdropColor)
|
|
||||||
view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop")
|
view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop")
|
||||||
titlebarView.addSubview(view)
|
titlebarView.addSubview(view)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user