Move un-zoom button into the tab/toolbar

This commit is contained in:
Pete Schaffner
2024-02-16 16:35:42 +01:00
parent a0c43a8566
commit 472a5c93ad
3 changed files with 109 additions and 37 deletions

View File

@ -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) {

View File

@ -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]
}
}

View File

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