From 472a5c93ad0e87efc607107160ae15973ca1e4cb Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Fri, 16 Feb 2024 16:35:42 +0100 Subject: [PATCH 1/9] Move un-zoom button into the tab/toolbar --- .../Terminal/TerminalController.swift | 79 +++++++++++++++++-- .../Features/Terminal/TerminalToolbar.swift | 55 +++++++------ .../Features/Terminal/TerminalView.swift | 12 +-- 3 files changed, 109 insertions(+), 37 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 5bb44f341..38d68ef32 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -3,6 +3,31 @@ import Cocoa import SwiftUI import GhosttyKit +fileprivate class ZoomButtonView: NSView { + let target: Any + let action: Selector + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + init(frame frameRect: NSRect, target: Any, selector: Selector) { + self.target = target + self.action = selector + + super.init(frame: frameRect) + + let zoomButton = NSButton(image: NSImage(systemSymbolName: "arrow.down.right.and.arrow.up.left.square.fill", accessibilityDescription: nil)!, target: target, action: selector) + + zoomButton.frame = bounds + zoomButton.isBordered = false + zoomButton.contentTintColor = .systemBlue + zoomButton.state = .on + zoomButton.imageScaling = .scaleProportionallyUpOrDown + addSubview(zoomButton) + } +} + /// The terminal controller is an NSWindowController that maps 1:1 to a terminal window. class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate, TerminalViewModel, @@ -54,8 +79,10 @@ class TerminalController: NSWindowController, NSWindowDelegate, /// 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. private var restorable: Bool = true - - init(_ ghostty: Ghostty.App, + + private var surfaceIsZoomed: Bool = false + + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: Ghostty.SplitNode? = nil ) { @@ -120,13 +147,13 @@ class TerminalController: NSWindowController, NSWindowDelegate, func relabelTabs() { // Reset this to false. It'll be set back to true later. tabListenForFrame = false - + guard let windows = self.window?.tabbedWindows else { return } // We only listen for frame changes if we have more than 1 window, // otherwise the accessory view doesn't matter. tabListenForFrame = windows.count > 1 - + for (index, window) in windows.enumerated().prefix(9) { let action = "goto_tab:\(index + 1)" guard let equiv = ghostty.config.keyEquivalent(for: action) else { @@ -141,10 +168,25 @@ class TerminalController: NSWindowController, NSWindowDelegate, let text = NSTextField(labelWithAttributedString: attributedString) text.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) text.postsFrameChangedNotifications = true - window.tab.accessoryView = text + + let stackView = NSStackView(views: [text]) +// stackView.setHuggingPriority(.defaultHigh, for: .horizontal) + + window.tab.accessoryView = stackView + } + + if surfaceIsZoomed { + guard let stackView = window?.tabGroup?.selectedWindow?.tab.accessoryView as? NSStackView else { return } + + var zoomButton: ZoomButtonView = ZoomButtonView(frame: NSRect(x: 0, y: 0, width: 20, height: 20), target: self, selector: #selector(splitZoom(_:))) + + zoomButton.translatesAutoresizingMaskIntoConstraints = false + zoomButton.widthAnchor.constraint(equalToConstant: 20).isActive = true + zoomButton.heightAnchor.constraint(equalToConstant: 20).isActive = true + stackView.addArrangedSubview(zoomButton) } } - + private func fixTabBar() { // 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 @@ -200,7 +242,17 @@ class TerminalController: NSWindowController, NSWindowDelegate, leaf.surface.focusDidChange(focused) } } - + + func windowDidUpdate(_ notification: Notification) { + updateToolbarZoomButton() + } + + private func updateToolbarZoomButton() { + guard let itemView = window?.toolbar?.items.last?.view as? ZoomButtonView else { return } + + itemView.alphaValue = surfaceIsZoomed ? 1 : 0 + } + //MARK: - NSWindowController override func windowWillLoad() { @@ -305,6 +357,11 @@ class TerminalController: NSWindowController, NSWindowDelegate, window.tabGroup?.removeWindow(window) } } + + guard let toolbarZoomItem = window.toolbar?.items.last else { return } + var zoomButton: ZoomButtonView = ZoomButtonView(frame: NSRect(x: 0, y: 0, width: 20, height: 20), target: self, selector: #selector(splitZoom(_:))) + + toolbarZoomItem.view = zoomButton } // Shows the "+" button in the tab bar, responds to that click. @@ -594,7 +651,13 @@ class TerminalController: NSWindowController, NSWindowDelegate, // we want to invalidate our state. invalidateRestorableState() } - + + func zoomStateDidChange(to: Bool) { + self.surfaceIsZoomed = to + updateToolbarZoomButton() + relabelTabs() + } + //MARK: - Clipboard Confirmation func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) { diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index b0857cb24..0bc389385 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -1,5 +1,9 @@ import Cocoa +fileprivate extension NSToolbarItem.Identifier { + static let zoom = NSToolbarItem.Identifier("zoom") +} + // Custom NSToolbar subclass that displays a centered window title, // in order to accommodate the titlebar tabs feature. class TerminalToolbar: NSToolbar, NSToolbarDelegate { @@ -31,32 +35,37 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { - guard itemIdentifier == Self.identifier else { - return NSToolbarItem(itemIdentifier: itemIdentifier) + var item: NSToolbarItem + + switch itemIdentifier { + case Self.identifier: + item = NSToolbarItem(itemIdentifier: itemIdentifier) + 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 .zoom: + item = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier("zoom")) + default: + item = NSToolbarItem(itemIdentifier: itemIdentifier) } - - 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 + + return item } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [Self.identifier, .space] + return [Self.identifier, .space, .zoom] } func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { @@ -64,7 +73,7 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { // 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] + return [Self.identifier, .space, .space, .zoom] } } diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index d0766c7ab..8e1f0dbdd 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -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: View { } } - if let zoomedSplit = zoomedSplit { - if zoomedSplit { - title = "🔍 " + title - } - } - return title } @@ -107,6 +104,9 @@ struct TerminalView: View { // in the hash value. self.delegate?.surfaceTreeDidChange() } + .onChange(of: zoomedSplit) { newValue in + self.delegate?.zoomStateDidChange(to: newValue ?? false) + } } } } From 021daeedb2caed26d8352eb4c49dc09da3055128 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Mon, 19 Feb 2024 10:15:33 +0100 Subject: [PATCH 2/9] Hide zoom button in toolbar when not zoomed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiding it via its alphaValue would allow the button to still respond to clicks… --- macos/Sources/Features/Terminal/TerminalController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 38d68ef32..3ddac967a 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -178,7 +178,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, if surfaceIsZoomed { guard let stackView = window?.tabGroup?.selectedWindow?.tab.accessoryView as? NSStackView else { return } - var zoomButton: ZoomButtonView = ZoomButtonView(frame: NSRect(x: 0, y: 0, width: 20, height: 20), target: self, selector: #selector(splitZoom(_:))) + let zoomButton: ZoomButtonView = ZoomButtonView(frame: NSRect(x: 0, y: 0, width: 20, height: 20), target: self, selector: #selector(splitZoom(_:))) zoomButton.translatesAutoresizingMaskIntoConstraints = false zoomButton.widthAnchor.constraint(equalToConstant: 20).isActive = true @@ -250,7 +250,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, private func updateToolbarZoomButton() { guard let itemView = window?.toolbar?.items.last?.view as? ZoomButtonView else { return } - itemView.alphaValue = surfaceIsZoomed ? 1 : 0 + itemView.isHidden = !surfaceIsZoomed } //MARK: - NSWindowController From dc60afc2610462df8391aebec805fe1587732b6b Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Fri, 23 Feb 2024 15:08:35 +0100 Subject: [PATCH 3/9] Get unzoom button working with standard title/tab bar To do this I forced a toolbar, so that we would have a place to put the button when no tabs were opened. I also took the opportunity to make the standard title/tab bar meld better with the terminal's background color, just as we do with titlebar tabs. --- .../Terminal/TerminalController.swift | 93 ++++++----------- .../Features/Terminal/TerminalToolbar.swift | 56 +++++++---- .../Features/Terminal/TerminalWindow.swift | 99 ++++++------------- 3 files changed, 99 insertions(+), 149 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 3ddac967a..bee5ec691 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -3,31 +3,6 @@ import Cocoa import SwiftUI import GhosttyKit -fileprivate class ZoomButtonView: NSView { - let target: Any - let action: Selector - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - init(frame frameRect: NSRect, target: Any, selector: Selector) { - self.target = target - self.action = selector - - super.init(frame: frameRect) - - let zoomButton = NSButton(image: NSImage(systemSymbolName: "arrow.down.right.and.arrow.up.left.square.fill", accessibilityDescription: nil)!, target: target, action: selector) - - zoomButton.frame = bounds - zoomButton.isBordered = false - zoomButton.contentTintColor = .systemBlue - zoomButton.state = .on - zoomButton.imageScaling = .scaleProportionallyUpOrDown - addSubview(zoomButton) - } -} - /// The terminal controller is an NSWindowController that maps 1:1 to a terminal window. class TerminalController: NSWindowController, NSWindowDelegate, TerminalViewDelegate, TerminalViewModel, @@ -169,21 +144,15 @@ class TerminalController: NSWindowController, NSWindowDelegate, text.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) text.postsFrameChangedNotifications = true - let stackView = NSStackView(views: [text]) -// stackView.setHuggingPriority(.defaultHigh, for: .horizontal) - - window.tab.accessoryView = stackView + window.tab.accessoryView = NSStackView(views: [text]) } if surfaceIsZoomed { - guard let stackView = window?.tabGroup?.selectedWindow?.tab.accessoryView as? NSStackView else { return } - - let zoomButton: ZoomButtonView = ZoomButtonView(frame: NSRect(x: 0, y: 0, width: 20, height: 20), target: self, selector: #selector(splitZoom(_:))) - - zoomButton.translatesAutoresizingMaskIntoConstraints = false - zoomButton.widthAnchor.constraint(equalToConstant: 20).isActive = true - zoomButton.heightAnchor.constraint(equalToConstant: 20).isActive = true - stackView.addArrangedSubview(zoomButton) + guard let stackView = window?.tabGroup?.selectedWindow?.tab.accessoryView as? NSStackView, + let buttonView = window?.toolbar?.items.first(where: { $0.itemIdentifier == .unZoom })?.view + else { return } + + stackView.addArrangedSubview(buttonView) } } @@ -243,14 +212,10 @@ class TerminalController: NSWindowController, NSWindowDelegate, } } - func windowDidUpdate(_ notification: Notification) { - updateToolbarZoomButton() - } + private func updateToolbarUnZoomButton() { + guard let buttonView = window?.toolbar?.items.first(where: { $0.itemIdentifier == .unZoom })?.view else { return } - private func updateToolbarZoomButton() { - guard let itemView = window?.toolbar?.items.last?.view as? ZoomButtonView else { return } - - itemView.isHidden = !surfaceIsZoomed + buttonView.isHidden = !surfaceIsZoomed } //MARK: - NSWindowController @@ -307,6 +272,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. @@ -317,19 +288,14 @@ 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 - ) } + + // Set a toolbar that is used with toolbar tabs + let toolbar = TerminalToolbar(identifier: "Toolbar") + toolbar.hasTitle = ghostty.config.macosTitlebarTabs + + window.toolbar = toolbar + window.toolbarStyle = .unifiedCompact // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( @@ -357,11 +323,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, window.tabGroup?.removeWindow(window) } } - - guard let toolbarZoomItem = window.toolbar?.items.last else { return } - var zoomButton: ZoomButtonView = ZoomButtonView(frame: NSRect(x: 0, y: 0, width: 20, height: 20), target: self, selector: #selector(splitZoom(_:))) - - toolbarZoomItem.view = zoomButton } // Shows the "+" button in the tab bar, responds to that click. @@ -370,7 +331,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, guard let surface = self.focusedSurface?.surface else { return } ghostty.newTab(surface: surface) } - + //MARK: - NSWindowDelegate // This is called when performClose is called on a window (NOT when close() @@ -450,7 +411,11 @@ class TerminalController: NSWindowController, NSWindowDelegate, } } } - + + func windowDidUpdate(_ notification: Notification) { + updateToolbarUnZoomButton() + } + // Called when the window will be encoded. We handle the data encoding here in the // window controller. func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { @@ -654,7 +619,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, func zoomStateDidChange(to: Bool) { self.surfaceIsZoomed = to - updateToolbarZoomButton() + updateToolbarUnZoomButton() relabelTabs() } diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index 0bc389385..811026526 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -1,13 +1,8 @@ import Cocoa -fileprivate extension NSToolbarItem.Identifier { - static let zoom = NSToolbarItem.Identifier("zoom") -} - // 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 { @@ -19,16 +14,18 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { titleTextField.stringValue = newValue } } - + + var hasTitle: Bool = false + override init(identifier: NSToolbar.Identifier) { super.init(identifier: identifier) delegate = self if #available(macOS 13.0, *) { - centeredItemIdentifiers.insert(Self.identifier) + centeredItemIdentifiers.insert(.titleText) } else { - centeredItemIdentifier = Self.identifier + centeredItemIdentifier = .titleText } } @@ -38,8 +35,8 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { var item: NSToolbarItem switch itemIdentifier { - case Self.identifier: - item = NSToolbarItem(itemIdentifier: itemIdentifier) + case .titleText: + item = NSToolbarItem(itemIdentifier: .titleText) item.view = self.titleTextField item.visibilityPriority = .user @@ -55,8 +52,24 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { item.maxSize = NSSize(width: 1024, height: self.titleTextField.intrinsicContentSize.height) item.isEnabled = true - case .zoom: - item = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier("zoom")) + case .unZoom: + item = NSToolbarItem(itemIdentifier: .unZoom) + + let view = NSView(frame: NSRect(x: 0, y: 0, width: 20, height: 20)) + view.translatesAutoresizingMaskIntoConstraints = false + view.widthAnchor.constraint(equalToConstant: 20).isActive = true + view.heightAnchor.constraint(equalToConstant: 20).isActive = true + + let button = NSButton(image: NSImage(systemSymbolName: "arrow.down.right.and.arrow.up.left.square.fill", accessibilityDescription: nil)!, target: nil, action: #selector(TerminalController.splitZoom(_:))) + + button.frame = view.bounds + button.isBordered = false + button.contentTintColor = .systemBlue + button.state = .on + button.imageScaling = .scaleProportionallyUpOrDown + view.addSubview(button) + + item.view = view default: item = NSToolbarItem(itemIdentifier: itemIdentifier) } @@ -65,15 +78,19 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [Self.identifier, .space, .zoom] + return [.titleText, .flexibleSpace, .space, .unZoom] } 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, .zoom] + // 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. + if hasTitle { + return [.titleText, .flexibleSpace, .space, .space, .unZoom] + } else { + return [.flexibleSpace, .unZoom] + } } } @@ -92,3 +109,8 @@ fileprivate class CenteredDynamicLabel: NSTextField { needsLayout = true } } + +extension NSToolbarItem.Identifier { + static let unZoom = NSToolbarItem.Identifier("UnZoom") + static let titleText = NSToolbarItem.Identifier("TitleText") +} diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 5fa06cfb0..db2a49b23 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -1,6 +1,17 @@ import Cocoa class TerminalWindow: NSWindow { + 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 + } + } + // 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 } @@ -35,13 +46,12 @@ 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 } } 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 @@ -66,72 +76,22 @@ 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 - } + override func awakeFromNib() { + super.awakeFromNib() - 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 + // 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 } } - - // 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) { @@ -217,13 +177,13 @@ class TerminalWindow: NSWindow { override func update() { super.update() - guard titlebarTabs else { return } + 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) { + if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self), titlebarTabs { windowButtonsBackdrop?.isHighlighted = index == 0 } @@ -239,7 +199,8 @@ class TerminalWindow: NSWindow { $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 } + + let isLightTheme = backgroundColor.isLightColor if newTabButtonImageLayer == nil { let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) @@ -294,7 +255,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) From 34f5bf4fe6d9c920e4ad208faad4fe7a9dea9bca Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Fri, 23 Feb 2024 22:30:03 +0100 Subject: [PATCH 4/9] Move unzoom button logic into TerminalWindow This fixed some bugs with keeping the correct button state when reordering tabs --- .../Terminal/TerminalController.swift | 47 +--- .../Features/Terminal/TerminalWindow.swift | 251 ++++++++++++------ 2 files changed, 180 insertions(+), 118 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bee5ec691..a17bd878f 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -55,8 +55,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, /// For example, terminals executing custom scripts are not restorable. private var restorable: Bool = true - private var surfaceIsZoomed: Bool = false - init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: Ghostty.SplitNode? = nil @@ -123,8 +121,8 @@ 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. tabListenForFrame = windows.count > 1 @@ -134,25 +132,8 @@ class TerminalController: NSWindowController, NSWindowDelegate, 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 = NSStackView(views: [text]) - } - - if surfaceIsZoomed { - guard let stackView = window?.tabGroup?.selectedWindow?.tab.accessoryView as? NSStackView, - let buttonView = window?.toolbar?.items.first(where: { $0.itemIdentifier == .unZoom })?.view - else { return } - - stackView.addArrangedSubview(buttonView) + window.keyEquivalent = "\(equiv)" } } @@ -212,12 +193,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, } } - private func updateToolbarUnZoomButton() { - guard let buttonView = window?.toolbar?.items.first(where: { $0.itemIdentifier == .unZoom })?.view else { return } - - buttonView.isHidden = !surfaceIsZoomed - } - //MARK: - NSWindowController override func windowWillLoad() { @@ -290,13 +265,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, } } - // Set a toolbar that is used with toolbar tabs - let toolbar = TerminalToolbar(identifier: "Toolbar") - toolbar.hasTitle = ghostty.config.macosTitlebarTabs - - window.toolbar = toolbar - window.toolbarStyle = .unifiedCompact - // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, @@ -412,10 +380,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, } } - func windowDidUpdate(_ notification: Notification) { - updateToolbarUnZoomButton() - } - // Called when the window will be encoded. We handle the data encoding here in the // window controller. func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { @@ -618,9 +582,8 @@ class TerminalController: NSWindowController, NSWindowDelegate, } func zoomStateDidChange(to: Bool) { - self.surfaceIsZoomed = to - updateToolbarUnZoomButton() - relabelTabs() + guard let window = window as? TerminalWindow else { return } + window.surfaceIsZoomed = to } //MARK: - Clipboard Confirmation diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index db2a49b23..5ec682a87 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -1,6 +1,9 @@ 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: { @@ -12,15 +15,97 @@ class TerminalWindow: NSWindow { } } + private var unZoomToolbarButton: NSButton? { + guard let button = toolbar?.items.first(where: { $0.itemIdentifier == .unZoom })?.view?.subviews.first as? NSButton + else { return nil } + + return button + } + + private let unZoomTabButton: NSButton = { + let button = NSButton() + button.target = nil + button.action = #selector(TerminalController.splitZoom(_:)) + button.translatesAutoresizingMaskIntoConstraints = false + button.widthAnchor.constraint(equalToConstant: 20).isActive = true + button.heightAnchor.constraint(equalToConstant: 20).isActive = true + button.isBordered = false + button.contentTintColor = .controlAccentColor + button.state = .on + button.image = NSImage(systemSymbolName: "arrow.down.right.and.arrow.up.left.square.fill", accessibilityDescription: nil)! + .withSymbolConfiguration(NSImage.SymbolConfiguration(scale: .large)) + + return button + }() + + 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 unZoomToolbarButton = self?.unZoomToolbarButton, let tabGroup = self?.tabGroup else { return } + + self?.unZoomTabButton.isHidden = !window.surfaceIsZoomed + self?.updateUnZoomToolbarButtonVisibility() + }, + + 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() + + // 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, unZoomTabButton]) + stackView.setHuggingPriority(.defaultHigh, for: .horizontal) + stackView.spacing = 3 + tab.accessoryView = stackView + + generateToolbar() + + _ = bindings + } + + 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() @@ -28,17 +113,99 @@ class TerminalWindow: NSWindow { super.becomeKey() - if titlebarTabs { - updateNewTabButtonOpacity() - } + updateNewTabButtonOpacity() + unZoomTabButton.isEnabled = true + unZoomTabButton.contentTintColor = .controlAccentColor + unZoomToolbarButton?.contentTintColor = .controlAccentColor } override func resignKey() { super.resignKey() - if titlebarTabs { - updateNewTabButtonOpacity() + updateNewTabButtonOpacity() + unZoomTabButton.isEnabled = false + unZoomTabButton.contentTintColor = .labelColor + unZoomToolbarButton?.contentTintColor = .tertiaryLabelColor + } + + override func update() { + super.update() + + updateUnZoomToolbarButtonVisibility() + + 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 func updateUnZoomToolbarButtonVisibility() { + guard let unZoomToolbarButton = unZoomToolbarButton, let tabGroup else { return } + + if tabGroup.isTabBarVisible { + unZoomToolbarButton.isHidden = true + } else { + unZoomToolbarButton.isHidden = !surfaceIsZoomed + } + } + + // 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") + terminalToolbar.hasTitle = titlebarTabs + + toolbar = terminalToolbar + toolbarStyle = .unifiedCompact + updateUnZoomToolbarButtonVisibility() } // MARK: - Titlebar Tabs @@ -47,6 +214,7 @@ class TerminalWindow: NSWindow { var titlebarTabs = false { didSet { self.titleVisibility = titlebarTabs ? .hidden : .visible + generateToolbar() } } @@ -76,22 +244,6 @@ class TerminalWindow: NSWindow { } } - override func awakeFromNib() { - super.awakeFromNib() - - // 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 - } - } - // 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) { @@ -174,59 +326,6 @@ class TerminalWindow: NSWindow { } } - override func update() { - super.update() - - 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 - } - // 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. From c189f855d818b48eaf7e4a8789179a823bd40642 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Sat, 24 Feb 2024 10:29:14 +0100 Subject: [PATCH 5/9] Reorganize a bit --- .../Features/Terminal/TerminalWindow.swift | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 5ec682a87..726e5bd13 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -185,6 +185,23 @@ class TerminalWindow: NSWindow { // 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 updateUnZoomToolbarButtonVisibility() { guard let unZoomToolbarButton = unZoomToolbarButton, let tabGroup else { return } @@ -220,7 +237,6 @@ class TerminalWindow: NSWindow { private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil private var windowDragHandle: WindowDragView? = nil - private var newTabButtonImageLayer: VibrantLayer? = nil // The tab bar controller ID from macOS static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController") @@ -326,21 +342,6 @@ class TerminalWindow: NSWindow { } } - // 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 { From 447310425db26b019e53e21ce6640973250dd8e2 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Sat, 24 Feb 2024 18:54:01 +0100 Subject: [PATCH 6/9] Fix wrong tint color and improve tab labeling --- macos/Sources/Features/Terminal/TerminalController.swift | 7 +++---- macos/Sources/Features/Terminal/TerminalToolbar.swift | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a17bd878f..863e9f88d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -129,11 +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 - } - window.keyEquivalent = "\(equiv)" + if let equiv = ghostty.config.keyEquivalent(for: action) { + window.keyEquivalent = "\(equiv)" + } } } diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index 811026526..1f9980b9d 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -64,7 +64,7 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { button.frame = view.bounds button.isBordered = false - button.contentTintColor = .systemBlue + button.contentTintColor = .controlAccentColor button.state = .on button.imageScaling = .scaleProportionallyUpOrDown view.addSubview(button) From 55621c214cef92a55fc9e26c918e8ac181b81171 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Sun, 25 Feb 2024 12:40:43 +0100 Subject: [PATCH 7/9] Rename "Un-zoom" to "Reset Zoom" and add tooltips --- .../Features/Terminal/TerminalToolbar.swift | 14 ++++--- .../Features/Terminal/TerminalWindow.swift | 40 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index 1f9980b9d..42c9c8724 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -52,8 +52,8 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { item.maxSize = NSSize(width: 1024, height: self.titleTextField.intrinsicContentSize.height) item.isEnabled = true - case .unZoom: - item = NSToolbarItem(itemIdentifier: .unZoom) + case .resetZoom: + item = NSToolbarItem(itemIdentifier: .resetZoom) let view = NSView(frame: NSRect(x: 0, y: 0, width: 20, height: 20)) view.translatesAutoresizingMaskIntoConstraints = false @@ -67,6 +67,8 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { button.contentTintColor = .controlAccentColor button.state = .on button.imageScaling = .scaleProportionallyUpOrDown + button.allowsExpansionToolTips = true + button.toolTip = "Reset Zoom" view.addSubview(button) item.view = view @@ -78,7 +80,7 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.titleText, .flexibleSpace, .space, .unZoom] + return [.titleText, .flexibleSpace, .space, .resetZoom] } func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { @@ -87,9 +89,9 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { // 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. if hasTitle { - return [.titleText, .flexibleSpace, .space, .space, .unZoom] + return [.titleText, .flexibleSpace, .space, .space, .resetZoom] } else { - return [.flexibleSpace, .unZoom] + return [.flexibleSpace, .resetZoom] } } } @@ -111,6 +113,6 @@ fileprivate class CenteredDynamicLabel: NSTextField { } extension NSToolbarItem.Identifier { - static let unZoom = NSToolbarItem.Identifier("UnZoom") + static let resetZoom = NSToolbarItem.Identifier("ResetZoom") static let titleText = NSToolbarItem.Identifier("TitleText") } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 726e5bd13..d15a02812 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -15,14 +15,14 @@ class TerminalWindow: NSWindow { } } - private var unZoomToolbarButton: NSButton? { - guard let button = toolbar?.items.first(where: { $0.itemIdentifier == .unZoom })?.view?.subviews.first as? NSButton + private var resetZoomToolbarButton: NSButton? { + guard let button = toolbar?.items.first(where: { $0.itemIdentifier == .resetZoom })?.view?.subviews.first as? NSButton else { return nil } return button } - private let unZoomTabButton: NSButton = { + private let resetZoomTabButton: NSButton = { let button = NSButton() button.target = nil button.action = #selector(TerminalController.splitZoom(_:)) @@ -30,6 +30,8 @@ class TerminalWindow: NSWindow { button.widthAnchor.constraint(equalToConstant: 20).isActive = true button.heightAnchor.constraint(equalToConstant: 20).isActive = true button.isBordered = false + button.allowsExpansionToolTips = true + button.toolTip = "Reset Zoom" button.contentTintColor = .controlAccentColor button.state = .on button.image = NSImage(systemSymbolName: "arrow.down.right.and.arrow.up.left.square.fill", accessibilityDescription: nil)! @@ -48,10 +50,10 @@ class TerminalWindow: NSWindow { private lazy var bindings = [ observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in - guard let unZoomToolbarButton = self?.unZoomToolbarButton, let tabGroup = self?.tabGroup else { return } + guard let resetZoomToolbarButton = self?.resetZoomToolbarButton, let tabGroup = self?.tabGroup else { return } - self?.unZoomTabButton.isHidden = !window.surfaceIsZoomed - self?.updateUnZoomToolbarButtonVisibility() + self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed + self?.updateResetZoomToolbarButtonVisibility() }, observe(\.keyEquivalent, options: [.initial, .new]) { [weak self] window, _ in @@ -88,7 +90,7 @@ class TerminalWindow: NSWindow { } // Create the tab accessory view that houses the key-equivalent label and optional un-zoom button - let stackView = NSStackView(views: [keyEquivalentLabel, unZoomTabButton]) + let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) stackView.setHuggingPriority(.defaultHigh, for: .horizontal) stackView.spacing = 3 tab.accessoryView = stackView @@ -114,24 +116,24 @@ class TerminalWindow: NSWindow { super.becomeKey() updateNewTabButtonOpacity() - unZoomTabButton.isEnabled = true - unZoomTabButton.contentTintColor = .controlAccentColor - unZoomToolbarButton?.contentTintColor = .controlAccentColor + resetZoomTabButton.isEnabled = true + resetZoomTabButton.contentTintColor = .controlAccentColor + resetZoomToolbarButton?.contentTintColor = .controlAccentColor } override func resignKey() { super.resignKey() updateNewTabButtonOpacity() - unZoomTabButton.isEnabled = false - unZoomTabButton.contentTintColor = .labelColor - unZoomToolbarButton?.contentTintColor = .tertiaryLabelColor + resetZoomTabButton.isEnabled = false + resetZoomTabButton.contentTintColor = .labelColor + resetZoomToolbarButton?.contentTintColor = .tertiaryLabelColor } override func update() { super.update() - updateUnZoomToolbarButtonVisibility() + updateResetZoomToolbarButtonVisibility() titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none @@ -202,13 +204,13 @@ class TerminalWindow: NSWindow { newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5 } - private func updateUnZoomToolbarButtonVisibility() { - guard let unZoomToolbarButton = unZoomToolbarButton, let tabGroup else { return } + private func updateResetZoomToolbarButtonVisibility() { + guard let resetZoomToolbarButton = resetZoomToolbarButton, let tabGroup else { return } if tabGroup.isTabBarVisible { - unZoomToolbarButton.isHidden = true + resetZoomToolbarButton.isHidden = true } else { - unZoomToolbarButton.isHidden = !surfaceIsZoomed + resetZoomToolbarButton.isHidden = !surfaceIsZoomed } } @@ -222,7 +224,7 @@ class TerminalWindow: NSWindow { toolbar = terminalToolbar toolbarStyle = .unifiedCompact - updateUnZoomToolbarButtonVisibility() + updateResetZoomToolbarButtonVisibility() } // MARK: - Titlebar Tabs From b6bfb9dac513972230a87e60363f346e7ac7b5d1 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Sun, 25 Feb 2024 21:47:15 +0100 Subject: [PATCH 8/9] Don't use a toolbar for the traditional title/tab bar I forgot I can use a `NSTitlebarAccessoryViewController` to house the button --- .../ResetZoom.imageset/Contents.json | 15 +++ .../ResetZoom.imageset/ResetZoom.pdf | Bin 0 -> 4795 bytes .../Features/Terminal/TerminalToolbar.swift | 26 +--- .../Features/Terminal/TerminalWindow.swift | 112 ++++++++++++------ 4 files changed, 89 insertions(+), 64 deletions(-) create mode 100644 macos/Assets.xcassets/ResetZoom.imageset/Contents.json create mode 100644 macos/Assets.xcassets/ResetZoom.imageset/ResetZoom.pdf diff --git a/macos/Assets.xcassets/ResetZoom.imageset/Contents.json b/macos/Assets.xcassets/ResetZoom.imageset/Contents.json new file mode 100644 index 000000000..b5bca19ac --- /dev/null +++ b/macos/Assets.xcassets/ResetZoom.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ResetZoom.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/macos/Assets.xcassets/ResetZoom.imageset/ResetZoom.pdf b/macos/Assets.xcassets/ResetZoom.imageset/ResetZoom.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fa36790fc94eb8396b6ea25438d34198f23fedb2 GIT binary patch literal 4795 zcmai&cTf{++lOgN5djtH>WUPpDTFRP5D=6mNT?o z%{xr{b=QuPBwL}#5bix~JGa+bi<;b_`q?viex0SP;~?*AAVlS8ubcOIIgtmG6bEWTz>;c}O`DgBn< z`)t#JDu@MOmK+~KK94lUHK4B*f!Ef!-cG3P)%AB>{*YE0BO>)V(&?UQ?YT|HZ_b%H zUecO+0guR#FHMvd8BxLJiy*1vlew)wL{bFwg18Md2TSb|OyVSbBTckA)wx5+Tv1I{ zKonC4*6npIXFLj#lQ8y7t0)0>+3}`rN}gn~saNc=H`eJVF_RCyCDvU@dlq;H{?SlQ z-gY-O2@lT&>qz_Wj1F^f5f{CllcUAuHaiFhezrO@0mh5UxJZ50zJu!RH~j^V&=Wxl zswb@idOS-9B_41V_-hs1D7C8MJSAV!{>al;&C-nBBkJ)sTybpEeYH@oEJaXC#L5}d zD`wfuw&8p~hFbFS`+){{#IbfQjJ4S7%`9y30}@N>UYYZVDfyw!TQ#cPIcr}_^~N}q zY8#P!T_XH!v$mR9aowql5s6qumpaWJYQ%984*j8E_5n6Rx50z+S_hmtSt#uR@6+Jm zsMm?`m$x=D?)*gao<)ubIo1`iKjoYwpH9fwGy%8Q3DxUM=dlkJ85Bsq3TbL78xVwC z)J$X^ON=RB_(*nj22l4Dd)Pj9_dWDhZnJ*lSY(B}@zB=I$Z#4@jZo~S7ai(%?a&oV zpNtm;;s7RPW{qZftf|5seXbYF*(Jzrrsr2ngeTpg&={l^Wr;4FT(N z(;+2|W->Z%Ctl8q^Fclzqb$G$RT%eD8hDxLK3xDkJ_Sh?{c zV`oEuZ;Fz+u_Ro3YGZpn!9PFgS}pTv{h>B8ZeI7W_^>v=wzu+-`dk=|78AzJ?oUWz z-0Y6f2KfzfH}1G&0FVL7;nx|DaU%dw01OxbAR~+i&J%Bo@c>|d1R6Lu0>+Kt0UROH z=!kuP&O2KF3z-IZoUJj209XJZ1GpAo1%NbgE;zigJIWRV95GeH0|H3@IpDV$GQZ8Z z4oCqYwIlSpp$HfN1jl$`Z81g&_5Z>bDg%N3Z#@4_7A*kQ$Qu$s!?$9=l@1T*SDSe6<4x5awP(`Pyww`sW8s9DUY z>n82(=@2_`n!X=orIeHc?dI~WZ<2v_q*3u&10j!r!+;omSq*{XR7{%vGFSYCIA>_$ zjmw+3p@($s8wn!NPzaPGi!r;Hf2#3?%U1rZ7^y8eNoYEY2ie8_4wgYA8H}e2PUaBl zvMiGYKRe{yqsNeM=%iB3Y+vr}-AU7<0<)*sbj}H7xr9SC*}1=kf2W~hh;NjChi8f0 zm3Pdb^2u|Uj-U(ILFb(sOvcW9ZMEN9D>vRnL&V}6u1!Al(W7isFh2rw2EM>0YEla# zT~i!mmP`1SmyrXrj=QzP6X*Sn50qMc_f6J>mM9r�=zw%Th6j%t5s!VE*J>c};=| zRfX$u)tSj+{S|=f`!TAD@wD~xp)S_3jbE6BPed*)xp+`#I!lUumEF(0k87_DmETV3 z__lAVn8&FSbV=u|6h?CmP*k(JNs*}flXloEp%dF#E3;Akd_%)s} zINXsGawW*@v7{YSa6tcL0GEs~J-2_lGrOaw4V7Pryg`)?eJf6hO5+l(fSZahjo+dL z8-%H~=AC0n%nOfbsPE6Yy$dHSBittS)cB2Ox0s{t@PgUm+&L4$F7-60AJY?_GQol* z!8GExPVxmxxzmmVRKbDTY_y5AlG-#_Rk~Md%0pC4YLFpnuLn-cQSq`i}tY_yM(F@Tp{)Uew7UsyBhdWr}?)=bYqSr4kRZc^}81+R8xNt!2i*y9QT(Y$iZ`>Y{TDe=bd- zX6j>>T;8`)+A+7MX^)3xp^~{cnz^UKkiYoe0Rb0*8*Drba|{Q; zr{2R1g9`Ks{Gn`j8ZFTxFCPu5z0=_nx*o@_6Dq)IBp{Mf5p+YxiQh^{Nr-$gu1&!X zf#*W1TI-hzWCAvVtg$Kv@t1|z^{?`2st;e6zxr17j_MtaXGSLr-1^L(Cs(JN>yzPr zSN%YUIH{!dq^Y)&xS|;6gvF%0g8K1;N#02|l9|Fb7Xz;T%==MAV_2_RZyc0UW}2ln zS@B*eL`O3OmO||{prtLpX>L5orIiFPQy<7{gOpdlzfVJ z1|{7Fb~ybbY@w_=+_!4aX|rKLX(5g$m8XV>o5zGF3Dzvlo$8j_mO7I<2WzQtG(Yj0 z&V0sv?zJ^IA>~`=4ST`zNcpUi(&c=KZspvuBBc`Bf^sv5oa^OXXRKl+Eo9XyEh~+^ z)%A5jW+0mvhAUahhM6UqxwVK|i-7jJkHE;SNVO;R1z8bUwWthK&ZO<7=P^ayqB-U{ zip_GiZ*E7dDNk7lm4&D4#Og$~T{9l)8^3X@no>bwcAj+AAcvbeFtqWA@*2=bVjYsEs@Uc*RZsqrIKX$HJY%Xfe zY6tTphAt`iRd6>vsnIIjR;N_wtj-jTzS`^0p08}a2Td2oKaS54kt*yi{D2Lv*s17M z7eqyriCIZ036nt$upF>Pbvw5Io5`7M)$SO*%N|I3Qu3ppdYjrrfhU3wkSEHWUX42- zFq!R!ou_6urc?UI@Eam4E<3r{mq%BBxIn}a`f$WZ>#$1$<3O@ zXji~hdw8`&t0M}-JK+{F-e>sKu&m~mWBJN#v-(?g!%4D4mj#PUlgn9`I)}CfBr)5LJ&`M>MVKTZsl_NNXzJ8?SyMU zFLe}c6TR|zcD{E3t=k69C2Foss85r{4V1;~h3Z;|0Gj~#vi4dG1rdUH*782uvtO}q zP_{reUgmvNZ1c;h+by_*&zl-|&lJrYNaeiUp_98PzxJhN=fX+`^OfN{5q{6-g%2{f z-h4EVFwYwJG!XtKR|b1m@9xBWU~l@lu5%J9PY&;G5VulN%iA4vDKTAV)1Js@di#G@ zZw^_D4Cf3}xJgj1u9)sgFD)w_?+8kIVZHViUpPwM|6!6;UYTd32c0OB;E7@0(=J-9zh7O7ni> z2i^3Atkx=>zH$eS2ewO7*}_cvq8ZOsN)GyW2JH*U8%s|IZ&Au0Iz)$7X@CwBc3 z9HSIc#I!2377!%}3UQY>=KFG^X|;ox)H@YYEAsQrc0zYiroZ<-*#X_wxa!=!n4)rZ zyr4#YZ(z%H&hG1UuQzrjFF#_us77Tg=pbv~cB3U`eo22vKOr+p#rq!PkMBN0uJ*GN z+rjJm|90ggRXuWB3jm~}p`niQz}Nvtu4)8W{iVN0zWkT|{;ks<0O%UV4vSL9`2ZGR z01QB200j2Shdtd000aQ(V(mNt3*gAcjR33PTv+yB12s?tlnc(`4^j8{)6oAGaJk#W zm;C*RK6s2h6Bv+Z0{`~_WTd5~qyc;2&kO>Ul>&aP0&ah0Fo=xYKQk~G3jJpWk&^x& zy;A?mATTK8pS>_BO#a`Q{BQRq;89o?4F1 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 + } + // MARK: - Titlebar Tabs // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { didSet { self.titleVisibility = titlebarTabs ? .hidden : .visible - generateToolbar() + if titlebarTabs { + generateToolbar() + } } } From 21ed1187a4f4542a0b51f77f8745d50b610aef0a Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Mon, 26 Feb 2024 10:03:14 +0100 Subject: [PATCH 9/9] Select and uzoom when clicking button in unselected tab --- .../Features/Terminal/TerminalWindow.swift | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index c8061d7fa..c8e194644 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -17,7 +17,11 @@ class TerminalWindow: NSWindow { private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() - private lazy var resetZoomTabButton: 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 } @@ -115,7 +119,6 @@ class TerminalWindow: NSWindow { super.becomeKey() updateNewTabButtonOpacity() - resetZoomTabButton.isEnabled = true resetZoomTabButton.contentTintColor = .controlAccentColor resetZoomToolbarButton.contentTintColor = .controlAccentColor } @@ -124,8 +127,7 @@ class TerminalWindow: NSWindow { super.resignKey() updateNewTabButtonOpacity() - resetZoomTabButton.isEnabled = false - resetZoomTabButton.contentTintColor = .labelColor + resetZoomTabButton.contentTintColor = .secondaryLabelColor resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor } @@ -259,6 +261,20 @@ class TerminalWindow: NSWindow { 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 // Used by the window controller to enable/disable titlebar tabs.