From fbac2d98107c3c3a011eea6ad32b5bf56d54e1d0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jan 2024 09:59:23 -0800 Subject: [PATCH] macos: titlebar tab logic shuffling --- .../Terminal/TerminalController.swift | 42 +-- .../Features/Terminal/TerminalWindow.swift | 287 ++++++++++-------- macos/Sources/Ghostty/Ghostty.Config.swift | 2 +- 3 files changed, 170 insertions(+), 161 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 03665c955..119d672e7 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -206,30 +206,16 @@ class TerminalController: NSWindowController, NSWindowDelegate, window.center() // 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 - if (self.ghostty.config.macosTitlebarTabs) { - window.titlebarTabs = true - window.titlebarAppearsTransparent = true - - // 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. - 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 - } - } + window.titlebarTabs = ghostty.config.macosTitlebarTabs + window.setTitlebarBackground( + window + .backgroundColor + .withAlphaComponent(ghostty.config.backgroundOpacity) + .cgColor + ) // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( @@ -237,14 +223,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, viewModel: 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 // its own tabbing so we DONT want this behavior. This detects this scenario and undoes @@ -327,10 +305,6 @@ class TerminalController: NSWindowController, NSWindowDelegate, func windowDidBecomeKey(_ notification: Notification) { 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 diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index bcafc0e96..e9310535a 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -1,60 +1,89 @@ 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 { // 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: - NSWindow + + override func becomeKey() { + // 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. + if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 { + hideCustomTabBarViews() + } + + super.becomeKey() + } + + // MARK: - Titlebar Tabs // Used by the window controller to enable/disable titlebar tabs. - public var titlebarTabs = false - + 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. + func setTitlebarBackground(_ color: CGColor) { + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }) else { return } + + titlebarContainer.wantsLayer = true + titlebarContainer.layer?.backgroundColor = 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) { - var isTabBar = false - if (self.titlebarTabs && ( - 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. + let isTabBar = self.titlebarTabs && ( + childViewController.layoutAttribute == .bottom || + childViewController.identifier == Self.TabBarController + ) + + if (isTabBar) { + // Ensure it has the right layoutAttribute to force it next to our titlebar + childViewController.layoutAttribute = .right + + // Hide the title text if the tab bar is showing since we show it in the tab titleVisibility = .hidden - // Mark the controller for future reference (it gets re-used sometimes) - childViewController.identifier = TabBarController - isTabBar = true + + // Mark the controller for future reference so we can easily find it. Otherwise + // the tab bar has no ID by default. + childViewController.identifier = Self.TabBarController } + super.addTitlebarAccessoryViewController(childViewController) + if (isTabBar) { pushTabsToTitlebar(childViewController) } @@ -63,103 +92,35 @@ class TerminalWindow: NSWindow { override func removeTitlebarAccessoryViewController(at index: Int) { let childViewController = titlebarAccessoryViewControllers[index] super.removeTitlebarAccessoryViewController(at: index) - if (childViewController.layoutAttribute == .right) { + if (childViewController.identifier == Self.TabBarController) { 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 { - hideCustomTabBarViews() - } - } - - // Assign a background color to the titlebar area. - public func setTitlebarBackground(_ color: CGColor) { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - - titlebarContainer.wantsLayer = true - titlebarContainer.layer?.backgroundColor = color - } - - private var windowButtonsBackdrop: NSView? = nil - - private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) { - guard windowButtonsBackdrop == nil else { return } - - windowButtonsBackdrop = NSView() - - guard let windowButtonsBackdrop = windowButtonsBackdrop else { return } - - windowButtonsBackdrop.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop") - 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 - - private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) { - guard windowDragHandle == nil else { return } - - windowDragHandle = WindowDragView() - - guard let windowDragHandle = windowDragHandle else { return } - - windowDragHandle.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle") - 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. - public func hideCustomTabBarViews() { - // Hide the window buttons backdrop. - windowButtonsBackdrop?.isHidden = true - // Hide the window drag handle. - windowDragHandle?.isHidden = true - // Enable the window title text. - titleVisibility = .visible - } + + // To be called immediately after the tab bar is disabled. + private func hideCustomTabBarViews() { + // Hide the window buttons backdrop. + windowButtonsBackdrop?.isHidden = true + + // Hide the window drag handle. + windowDragHandle?.isHidden = true + + // Enable the window title text. + titleVisibility = .visible + } private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) { let accessoryView = tabBarController.view guard let accessoryClipView = accessoryView.superview else { return } guard let titlebarView = accessoryClipView.superview else { return } - guard titlebarView.className == "NSTitlebarView" else { return } - guard let toolbarView = titlebarView.subviews.first(where: { $0.className == "NSToolbarView" }) else { return } addWindowButtonsBackdrop(titlebarView: titlebarView, toolbarView: toolbarView) - windowButtonsBackdrop?.isHidden = false guard let windowButtonsBackdrop = windowButtonsBackdrop else { return } + windowButtonsBackdrop.isHidden = false addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView) windowDragHandle?.isHidden = false @@ -186,7 +147,52 @@ class TerminalWindow: NSWindow { self.markHierarchyForLayout(accessoryView) } } + + 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) { view.needsUpdateConstraints = 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) + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 77c08722c..b636fbcf3 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -203,7 +203,7 @@ extension Ghostty { var v = false; let key = "macos-titlebar-tabs" _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v + return true } var backgroundColor: Color {