macos: titlebar tab logic shuffling

This commit is contained in:
Mitchell Hashimoto
2024-01-31 09:59:23 -08:00
parent dacbdf3f38
commit fbac2d9810
3 changed files with 170 additions and 161 deletions

View File

@ -206,30 +206,16 @@ class TerminalController: NSWindowController, NSWindowDelegate,
window.center() window.center()
// Set the background color of the window // Set the background color of the window
window.backgroundColor = NSColor(self.ghostty.config.backgroundColor) window.backgroundColor = NSColor(ghostty.config.backgroundColor)
// Handle titlebar tabs config option // Handle titlebar tabs config option
if (self.ghostty.config.macosTitlebarTabs) { window.titlebarTabs = ghostty.config.macosTitlebarTabs
window.titlebarTabs = true window.setTitlebarBackground(
window.titlebarAppearsTransparent = true window
.backgroundColor
// We use the toolbar to anchor our tab bar positions in the titlebar, .withAlphaComponent(ghostty.config.backgroundOpacity)
// so we make sure it's the right size/position, and exists. .cgColor
window.toolbarStyle = .unifiedCompact )
if (window.toolbar == nil) {
window.toolbar = NSToolbar(identifier: "Toolbar")
}
} else {
window.titlebarTabs = false
window.titlebarAppearsTransparent = false
// "expanded" places the toolbar below the titlebar, so setting this style and
// removing the toolbar ensures that the titlebar will be the default height.
window.toolbarStyle = .expanded
if (window.toolbar != nil) {
window.toolbar = nil
}
}
// 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(
@ -238,14 +224,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
delegate: self delegate: self
)) ))
// Give the titlebar a custom background color to account for transparent windows.
window.setTitlebarBackground(
window
.backgroundColor
.withAlphaComponent(self.ghostty.config.backgroundOpacity)
.cgColor
)
// In various situations, macOS automatically tabs new windows. Ghostty handles // In various situations, macOS automatically tabs new windows. Ghostty handles
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes // its own tabbing so we DONT want this behavior. This detects this scenario and undoes
// it. // it.
@ -327,10 +305,6 @@ class TerminalController: NSWindowController, NSWindowDelegate,
func windowDidBecomeKey(_ notification: Notification) { func windowDidBecomeKey(_ notification: Notification) {
self.relabelTabs() self.relabelTabs()
// Fix for titlebar tabs, see comment on implementation of fixUntabbedWindow for details.
guard let window = window as? TerminalWindow else { return }
window.fixUntabbedWindow()
} }
// 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

View File

@ -1,85 +1,59 @@
import Cocoa import Cocoa
// Passes mouseDown events from this view to window.performDrag so that you can drag the window by it.
class WindowDragView: NSView {
override public func mouseDown(with event: NSEvent) {
// Drag the window for single left clicks, double clicks should bypass the drag handle.
if (event.type == .leftMouseDown && event.clickCount == 1) {
window?.performDrag(with: event)
NSCursor.closedHand.set()
} else {
super.mouseDown(with: event)
}
}
override public func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
window?.disableCursorRects()
NSCursor.openHand.set()
}
override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
window?.enableCursorRects()
NSCursor.arrow.set()
}
override func resetCursorRects() {
addCursorRect(bounds, cursor: .openHand)
}
}
let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController")
class TerminalWindow: NSWindow { class TerminalWindow: NSWindow {
// 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 }
// Used by the window controller to enable/disable titlebar tabs. // MARK: - NSWindow
public var titlebarTabs = false
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { override func becomeKey() {
var isTabBar = false // This is required because the removeTitlebarAccessoryViewControlle hook does not
if (self.titlebarTabs && ( // catch the creation of a new window by "tearing off" a tab from a tabbed window.
childViewController.layoutAttribute == .bottom ||
childViewController.identifier == TabBarController)
) {
// Ensure it has the right layoutAttribute
childViewController.layoutAttribute = .right
// Hide the title text if the tab bar is showing.
titleVisibility = .hidden
// Mark the controller for future reference (it gets re-used sometimes)
childViewController.identifier = TabBarController
isTabBar = true
}
super.addTitlebarAccessoryViewController(childViewController)
if (isTabBar) {
pushTabsToTitlebar(childViewController)
}
}
override func removeTitlebarAccessoryViewController(at index: Int) {
let childViewController = titlebarAccessoryViewControllers[index]
super.removeTitlebarAccessoryViewController(at: index)
if (childViewController.layoutAttribute == .right) {
hideCustomTabBarViews()
}
}
// This is a hack - provide a function for the window controller to call in windowDidBecomeKey
// to check if it's no longer tabbed and fix its appearing if so. 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.
public func fixUntabbedWindow() {
if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 { if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 {
hideCustomTabBarViews() hideCustomTabBarViews()
} }
super.becomeKey()
}
// MARK: - Titlebar Tabs
// Used by the window controller to enable/disable titlebar tabs.
var titlebarTabs = false {
didSet {
changedTitlebarTabs(to: titlebarTabs)
}
}
private var windowButtonsBackdrop: NSView? = nil
private var windowDragHandle: WindowDragView? = nil
// The tab bar controller ID from macOS
static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController")
/// This is called by titlebarTabs changing so that we can setup the rest of our window
private func changedTitlebarTabs(to newValue: Bool) {
self.titlebarAppearsTransparent = newValue
if (newValue) {
// 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 = NSToolbar(identifier: "Toolbar")
}
} 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
}
} }
// Assign a background color to the titlebar area. // Assign a background color to the titlebar area.
public func setTitlebarBackground(_ color: CGColor) { func setTitlebarBackground(_ color: CGColor) {
guard let titlebarContainer = contentView?.superview?.subviews.first(where: { guard let titlebarContainer = contentView?.superview?.subviews.first(where: {
$0.className == "NSTitlebarContainerView" $0.className == "NSTitlebarContainerView"
}) else { return } }) else { return }
@ -88,60 +62,49 @@ class TerminalWindow: NSWindow {
titlebarContainer.layer?.backgroundColor = color titlebarContainer.layer?.backgroundColor = color
} }
private var windowButtonsBackdrop: NSView? = nil // 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) {
let isTabBar = self.titlebarTabs && (
childViewController.layoutAttribute == .bottom ||
childViewController.identifier == Self.TabBarController
)
private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) { if (isTabBar) {
guard windowButtonsBackdrop == nil else { return } // Ensure it has the right layoutAttribute to force it next to our titlebar
childViewController.layoutAttribute = .right
windowButtonsBackdrop = NSView() // Hide the title text if the tab bar is showing since we show it in the tab
titleVisibility = .hidden
guard let windowButtonsBackdrop = windowButtonsBackdrop else { return } // Mark the controller for future reference so we can easily find it. Otherwise
// the tab bar has no ID by default.
windowButtonsBackdrop.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop") childViewController.identifier = Self.TabBarController
titlebarView.addSubview(windowButtonsBackdrop)
windowButtonsBackdrop.translatesAutoresizingMaskIntoConstraints = false
windowButtonsBackdrop.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true
windowButtonsBackdrop.rightAnchor.constraint(equalTo: toolbarView.leftAnchor, constant: 80).isActive = true
windowButtonsBackdrop.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true
windowButtonsBackdrop.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true
windowButtonsBackdrop.wantsLayer = true
windowButtonsBackdrop.layer?.backgroundColor = CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.45)
let topBorder = NSView()
windowButtonsBackdrop.addSubview(topBorder)
topBorder.translatesAutoresizingMaskIntoConstraints = false
topBorder.leftAnchor.constraint(equalTo: windowButtonsBackdrop.leftAnchor).isActive = true
topBorder.rightAnchor.constraint(equalTo: windowButtonsBackdrop.rightAnchor).isActive = true
topBorder.topAnchor.constraint(equalTo: windowButtonsBackdrop.topAnchor).isActive = true
topBorder.bottomAnchor.constraint(equalTo: windowButtonsBackdrop.topAnchor, constant: 1).isActive = true
topBorder.wantsLayer = true
topBorder.layer?.backgroundColor = CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.85)
} }
var windowDragHandle: WindowDragView? = nil super.addTitlebarAccessoryViewController(childViewController)
private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) { if (isTabBar) {
guard windowDragHandle == nil else { return } pushTabsToTitlebar(childViewController)
}
}
windowDragHandle = WindowDragView() override func removeTitlebarAccessoryViewController(at index: Int) {
let childViewController = titlebarAccessoryViewControllers[index]
guard let windowDragHandle = windowDragHandle else { return } super.removeTitlebarAccessoryViewController(at: index)
if (childViewController.identifier == Self.TabBarController) {
windowDragHandle.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle") hideCustomTabBarViews()
titlebarView.superview?.addSubview(windowDragHandle) }
windowDragHandle.translatesAutoresizingMaskIntoConstraints = false
windowDragHandle.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true
windowDragHandle.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true
windowDragHandle.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true
windowDragHandle.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true
} }
// To be called immediately after the tab bar is disabled. // To be called immediately after the tab bar is disabled.
public func hideCustomTabBarViews() { private func hideCustomTabBarViews() {
// Hide the window buttons backdrop. // Hide the window buttons backdrop.
windowButtonsBackdrop?.isHidden = true windowButtonsBackdrop?.isHidden = true
// Hide the window drag handle. // Hide the window drag handle.
windowDragHandle?.isHidden = true windowDragHandle?.isHidden = true
// Enable the window title text. // Enable the window title text.
titleVisibility = .visible titleVisibility = .visible
} }
@ -150,16 +113,14 @@ class TerminalWindow: NSWindow {
let accessoryView = tabBarController.view let accessoryView = tabBarController.view
guard let accessoryClipView = accessoryView.superview else { return } guard let accessoryClipView = accessoryView.superview else { return }
guard let titlebarView = accessoryClipView.superview else { return } guard let titlebarView = accessoryClipView.superview else { return }
guard titlebarView.className == "NSTitlebarView" else { return } guard titlebarView.className == "NSTitlebarView" else { return }
guard let toolbarView = titlebarView.subviews.first(where: { guard let toolbarView = titlebarView.subviews.first(where: {
$0.className == "NSToolbarView" $0.className == "NSToolbarView"
}) else { return } }) else { return }
addWindowButtonsBackdrop(titlebarView: titlebarView, toolbarView: toolbarView) addWindowButtonsBackdrop(titlebarView: titlebarView, toolbarView: toolbarView)
windowButtonsBackdrop?.isHidden = false
guard let windowButtonsBackdrop = windowButtonsBackdrop else { return } guard let windowButtonsBackdrop = windowButtonsBackdrop else { return }
windowButtonsBackdrop.isHidden = false
addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView) addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView)
windowDragHandle?.isHidden = false windowDragHandle?.isHidden = false
@ -187,6 +148,51 @@ class TerminalWindow: NSWindow {
} }
} }
private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) {
guard windowButtonsBackdrop == nil else { return }
let view = NSView()
view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop")
titlebarView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: toolbarView.leftAnchor, constant: 80).isActive = true
view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true
view.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true
view.wantsLayer = true
view.layer?.backgroundColor = CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.45)
let topBorder = NSView()
view.addSubview(topBorder)
topBorder.translatesAutoresizingMaskIntoConstraints = false
topBorder.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
topBorder.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
topBorder.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
topBorder.bottomAnchor.constraint(equalTo: view.topAnchor, constant: 1).isActive = true
topBorder.wantsLayer = true
topBorder.layer?.backgroundColor = CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.85)
windowButtonsBackdrop = view
}
private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) {
guard windowDragHandle == nil else { return }
let view = WindowDragView()
view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle")
titlebarView.superview?.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true
view.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true
view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true
windowDragHandle = view
}
// This forces this view and all subviews to update layout and redraw. This is
// a hack (see the caller).
private func markHierarchyForLayout(_ view: NSView) { private func markHierarchyForLayout(_ view: NSView) {
view.needsUpdateConstraints = true view.needsUpdateConstraints = true
view.needsLayout = true view.needsLayout = true
@ -197,3 +203,32 @@ class TerminalWindow: NSWindow {
} }
} }
} }
// Passes mouseDown events from this view to window.performDrag so that you can drag the window by it.
fileprivate class WindowDragView: NSView {
override public func mouseDown(with event: NSEvent) {
// Drag the window for single left clicks, double clicks should bypass the drag handle.
if (event.type == .leftMouseDown && event.clickCount == 1) {
window?.performDrag(with: event)
NSCursor.closedHand.set()
} else {
super.mouseDown(with: event)
}
}
override public func mouseEntered(with event: NSEvent) {
super.mouseEntered(with: event)
window?.disableCursorRects()
NSCursor.openHand.set()
}
override func mouseExited(with event: NSEvent) {
super.mouseExited(with: event)
window?.enableCursorRects()
NSCursor.arrow.set()
}
override func resetCursorRects() {
addCursorRect(bounds, cursor: .openHand)
}
}

View File

@ -203,7 +203,7 @@ extension Ghostty {
var v = false; var v = false;
let key = "macos-titlebar-tabs" let key = "macos-titlebar-tabs"
_ = ghostty_config_get(config, &v, key, UInt(key.count)) _ = ghostty_config_get(config, &v, key, UInt(key.count))
return v return true
} }
var backgroundColor: Color { var backgroundColor: Color {