From ba16d65d021d220b87fff39bed3b9209c9ea46f0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 30 Jan 2024 18:48:36 -0500 Subject: [PATCH 1/9] macOS: Added titlebar tabs --- .../Terminal/TerminalController.swift | 34 +++- .../Features/Terminal/TerminalWindow.swift | 181 ++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Config.swift | 8 + src/config/Config.zig | 7 + 4 files changed, 228 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 2a3e76fb7..84911e13b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -159,7 +159,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, } override func windowDidLoad() { - guard let window = window else { return } + guard let window = window as? TerminalWindow else { return } // Setting all three of these is required for restoration to work. window.isRestorable = restorable @@ -204,7 +204,33 @@ class TerminalController: NSWindowController, NSWindowDelegate, // Center the window to start, we'll move the window frame automatically // when cascading. window.center() - + + // Set the background color of the window + window.backgroundColor = NSColor(self.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 + } + } + // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, @@ -293,6 +319,10 @@ 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 9a74a7e2b..af7956d4d 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -1,8 +1,189 @@ 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 } + + // Used by the window controller to enable/disable titlebar tabs. + public var titlebarTabs = false + + 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. + 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 { + hideCustomTabBarViews() + } + } + + 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 + } + + 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 } + + addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView) + windowDragHandle?.isHidden = false + + accessoryClipView.translatesAutoresizingMaskIntoConstraints = false + accessoryClipView.leftAnchor.constraint(equalTo: windowButtonsBackdrop.rightAnchor).isActive = true + accessoryClipView.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true + accessoryClipView.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true + accessoryClipView.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true + accessoryClipView.needsLayout = true + + accessoryView.translatesAutoresizingMaskIntoConstraints = false + accessoryView.leftAnchor.constraint(equalTo: accessoryClipView.leftAnchor).isActive = true + accessoryView.rightAnchor.constraint(equalTo: accessoryClipView.rightAnchor).isActive = true + accessoryView.topAnchor.constraint(equalTo: accessoryClipView.topAnchor).isActive = true + accessoryView.heightAnchor.constraint(equalTo: accessoryClipView.heightAnchor).isActive = true + accessoryView.needsLayout = true + + // This is a horrible hack. During the transition while things are resizing to make room for + // new tabs or expand existing tabs to fill the empty space after one is closed, the centering + // of the tab titles can't be properly calculated, so we wait for 0.2 seconds and then mark + // the entire view hierarchy for the tab bar as dirty to fix the positioning... + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.markHierarchyForLayout(accessoryView) + } + } + + private func markHierarchyForLayout(_ view: NSView) { + view.needsUpdateConstraints = true + view.needsLayout = true + view.needsDisplay = true + view.setNeedsDisplay(view.bounds) + for subview in view.subviews { + markHierarchyForLayout(subview) + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 8b1146e14..77c08722c 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -197,6 +197,14 @@ extension Ghostty { _ = ghostty_config_get(config, &v, key, UInt(key.count)) return v } + + var macosTitlebarTabs: Bool { + guard let config = self.config else { return false } + var v = false; + let key = "macos-titlebar-tabs" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v + } var backgroundColor: Color { var rgb: UInt32 = 0 diff --git a/src/config/Config.zig b/src/config/Config.zig index b0f618ba3..6ee1bffa8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -891,6 +891,13 @@ keybind: Keybinds = .{}, /// * `false` - Use native macOS fullscreeen @"macos-non-native-fullscreen": NonNativeFullscreen = .false, +/// If `true`, places the tab bar in the titlebar for tabbed windows. +/// +/// This option intercepts the native tab bar view from macOS and forces it to use +/// different positioning. Because of this, it might be buggy or break entirely if +/// macOS changes the way its native tab bar view is constructed or managed. +@"macos-titlebar-tabs": bool = false, + /// If `true`, the *Option* key will be treated as *Alt*. This makes terminal /// sequences expecting *Alt* to work properly, but will break Unicode input /// sequences on macOS if you use them via the *Alt* key. You may set this to From dacbdf3f387b2eeb936ac239f6b9bb276b7ec036 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 31 Jan 2024 07:58:31 -0500 Subject: [PATCH 2/9] fix(macOS): set titlebar background color to support transparent windows with titlebar tabs --- .../Sources/Features/Terminal/TerminalController.swift | 8 ++++++++ macos/Sources/Features/Terminal/TerminalWindow.swift | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 84911e13b..03665c955 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -237,6 +237,14 @@ 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 diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index af7956d4d..bcafc0e96 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -78,6 +78,16 @@ class TerminalWindow: NSWindow { } } + // 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) { From fbac2d98107c3c3a011eea6ad32b5bf56d54e1d0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jan 2024 09:59:23 -0800 Subject: [PATCH 3/9] 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 { From 00661c13ab9d9f7068c71f1b9516afdb9aa42dcd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jan 2024 10:20:37 -0800 Subject: [PATCH 4/9] config: clarify some limitations --- src/config/Config.zig | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 6ee1bffa8..fc9fee414 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -882,20 +882,30 @@ keybind: Keybinds = .{}, /// using a new space. It's faster than the native fullscreen mode since it /// doesn't use animations. /// +/// Warning: tabs do not work with a non-native fullscreen window. This +/// can be fixed but is looking for contributors to help. See issue #392. +/// /// Allowable values are: /// /// * `visible-menu` - Use non-native macOS fullscreen, keep the menu bar visible -/// /// * `true` - Use non-native macOS fullscreen, hide the menu bar -/// /// * `false` - Use native macOS fullscreeen +/// @"macos-non-native-fullscreen": NonNativeFullscreen = .false, /// If `true`, places the tab bar in the titlebar for tabbed windows. /// +/// When this is true, the titlebar will also always appear even when +/// fullscreen (native fullscreen) with only one tab. This is not considered +/// a bug but if you'd like to improve this behavior then I'm open to it and +/// please contribute to the project. +/// /// This option intercepts the native tab bar view from macOS and forces it to use /// different positioning. Because of this, it might be buggy or break entirely if /// macOS changes the way its native tab bar view is constructed or managed. +/// This has been tested on macOS 14. +/// +/// This option only applies to new windows when changed. @"macos-titlebar-tabs": bool = false, /// If `true`, the *Option* key will be treated as *Alt*. This makes terminal From 939bb22615383d3009cd57509d5a04aad9258d1d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jan 2024 10:22:09 -0800 Subject: [PATCH 5/9] macos: do not hardcode true for titlebar tabs --- macos/Sources/Ghostty/Ghostty.Config.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index b636fbcf3..77c08722c 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 true + return v } var backgroundColor: Color { From 1a3d2d151ec1a6a94e43403519ae34a571060cff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jan 2024 12:06:58 -0800 Subject: [PATCH 6/9] macos: fix tabs vs spaces --- .../Terminal/TerminalController.swift | 12 +- .../Features/Terminal/TerminalWindow.swift | 144 +++++++++--------- 2 files changed, 78 insertions(+), 78 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 119d672e7..2ffb6a60f 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -204,11 +204,11 @@ class TerminalController: NSWindowController, NSWindowDelegate, // Center the window to start, we'll move the window frame automatically // when cascading. window.center() - - // Set the background color of the window - window.backgroundColor = NSColor(ghostty.config.backgroundColor) - - // Handle titlebar tabs config option + + // Set the background color of the window + window.backgroundColor = NSColor(ghostty.config.backgroundColor) + + // Handle titlebar tabs config option window.titlebarTabs = ghostty.config.macosTitlebarTabs window.setTitlebarBackground( window @@ -216,7 +216,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, .withAlphaComponent(ghostty.config.backgroundOpacity) .cgColor ) - + // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index e9310535a..fabbd8dc4 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -19,8 +19,8 @@ class TerminalWindow: NSWindow { } // MARK: - Titlebar Tabs - - // Used by the window controller to enable/disable titlebar tabs. + + // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { didSet { changedTitlebarTabs(to: titlebarTabs) @@ -32,7 +32,7 @@ class TerminalWindow: NSWindow { // 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 @@ -61,41 +61,41 @@ class TerminalWindow: NSWindow { 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) { - let isTabBar = self.titlebarTabs && ( + override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { + 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 + 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 + // 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 so we can easily find it. Otherwise + // 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) + super.addTitlebarAccessoryViewController(childViewController) - if (isTabBar) { - pushTabsToTitlebar(childViewController) - } - } - - override func removeTitlebarAccessoryViewController(at index: Int) { - let childViewController = titlebarAccessoryViewControllers[index] - super.removeTitlebarAccessoryViewController(at: index) + if (isTabBar) { + pushTabsToTitlebar(childViewController) + } + } + + override func removeTitlebarAccessoryViewController(at index: Int) { + let childViewController = titlebarAccessoryViewControllers[index] + super.removeTitlebarAccessoryViewController(at: index) if (childViewController.identifier == Self.TabBarController) { - hideCustomTabBarViews() - } - } + hideCustomTabBarViews() + } + } // To be called immediately after the tab bar is disabled. private func hideCustomTabBarViews() { @@ -108,45 +108,45 @@ class TerminalWindow: NSWindow { // 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) - guard let windowButtonsBackdrop = windowButtonsBackdrop else { return } + + 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) + guard let windowButtonsBackdrop = windowButtonsBackdrop else { return } windowButtonsBackdrop.isHidden = false - - addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView) - windowDragHandle?.isHidden = false - - accessoryClipView.translatesAutoresizingMaskIntoConstraints = false - accessoryClipView.leftAnchor.constraint(equalTo: windowButtonsBackdrop.rightAnchor).isActive = true - accessoryClipView.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true - accessoryClipView.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true - accessoryClipView.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true - accessoryClipView.needsLayout = true - - accessoryView.translatesAutoresizingMaskIntoConstraints = false - accessoryView.leftAnchor.constraint(equalTo: accessoryClipView.leftAnchor).isActive = true - accessoryView.rightAnchor.constraint(equalTo: accessoryClipView.rightAnchor).isActive = true - accessoryView.topAnchor.constraint(equalTo: accessoryClipView.topAnchor).isActive = true - accessoryView.heightAnchor.constraint(equalTo: accessoryClipView.heightAnchor).isActive = true - accessoryView.needsLayout = true - - // This is a horrible hack. During the transition while things are resizing to make room for - // new tabs or expand existing tabs to fill the empty space after one is closed, the centering - // of the tab titles can't be properly calculated, so we wait for 0.2 seconds and then mark - // the entire view hierarchy for the tab bar as dirty to fix the positioning... - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self.markHierarchyForLayout(accessoryView) - } - } + + addWindowDragHandle(titlebarView: titlebarView, toolbarView: toolbarView) + windowDragHandle?.isHidden = false + + accessoryClipView.translatesAutoresizingMaskIntoConstraints = false + accessoryClipView.leftAnchor.constraint(equalTo: windowButtonsBackdrop.rightAnchor).isActive = true + accessoryClipView.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true + accessoryClipView.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true + accessoryClipView.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true + accessoryClipView.needsLayout = true + + accessoryView.translatesAutoresizingMaskIntoConstraints = false + accessoryView.leftAnchor.constraint(equalTo: accessoryClipView.leftAnchor).isActive = true + accessoryView.rightAnchor.constraint(equalTo: accessoryClipView.rightAnchor).isActive = true + accessoryView.topAnchor.constraint(equalTo: accessoryClipView.topAnchor).isActive = true + accessoryView.heightAnchor.constraint(equalTo: accessoryClipView.heightAnchor).isActive = true + accessoryView.needsLayout = true + + // This is a horrible hack. During the transition while things are resizing to make room for + // new tabs or expand existing tabs to fill the empty space after one is closed, the centering + // of the tab titles can't be properly calculated, so we wait for 0.2 seconds and then mark + // the entire view hierarchy for the tab bar as dirty to fix the positioning... + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + self.markHierarchyForLayout(accessoryView) + } + } private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) { guard windowButtonsBackdrop == nil else { return } @@ -190,18 +190,18 @@ class TerminalWindow: NSWindow { 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 - view.needsDisplay = true - view.setNeedsDisplay(view.bounds) - for subview in view.subviews { - markHierarchyForLayout(subview) - } - } + private func markHierarchyForLayout(_ view: NSView) { + view.needsUpdateConstraints = true + view.needsLayout = true + view.needsDisplay = true + view.setNeedsDisplay(view.bounds) + for subview in view.subviews { + markHierarchyForLayout(subview) + } + } } // Passes mouseDown events from this view to window.performDrag so that you can drag the window by it. From 308f8cce3647eb1f52532723fda6502f71054310 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 31 Jan 2024 16:16:41 -0500 Subject: [PATCH 7/9] macOS: center window title when titlebar tabs enabled Uses a custom toolbar that populates itself with a centered text field and provides a method to set the text. --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ .../Terminal/TerminalController.swift | 8 +++- .../Features/Terminal/TerminalToolbar.swift | 44 +++++++++++++++++++ .../Features/Terminal/TerminalWindow.swift | 31 ++++++++++--- 4 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 macos/Sources/Features/Terminal/TerminalToolbar.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 8c8e327d4..eb8c236e7 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -65,6 +65,7 @@ A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; }; A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; + AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; /* End PBXBuildFile section */ @@ -125,6 +126,7 @@ A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; }; A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; + AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalToolbar.swift; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -281,6 +283,7 @@ A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */, + AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, ); path = Terminal; @@ -493,6 +496,7 @@ A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */, 8503D7C72A549C66006CFF3D /* FullScreenHandler.swift in Sources */, A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, + AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */, C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 2ffb6a60f..ab739a88f 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -482,7 +482,13 @@ class TerminalController: NSWindowController, NSWindowDelegate, } func titleDidChange(to: String) { - self.window?.title = to + guard let window = window as? TerminalWindow else { return } + + window.title = to + + // Custom toolbar-based title used when titlebar tabs are enabled. + guard let toolbar = window.toolbar as? TerminalToolbar else { return } + toolbar.setTitleText(to) } func cellSizeDidChange(to: NSSize) { diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift new file mode 100644 index 000000000..685db8b53 --- /dev/null +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -0,0 +1,44 @@ +// Custom NSToolbar subclass that displays a centered window title, +// in order to accommodate the titlebar tabs feature. + +import Foundation +import Cocoa +import SwiftUI + +class TerminalToolbar: NSToolbar, NSToolbarDelegate { + static private let TitleIdentifier = NSToolbarItem.Identifier("TitleText") + private let TitleTextField = NSTextField( + labelWithString: "👻 Ghostty" + ) + + func setTitleText(_ text: String) { + self.TitleTextField.stringValue = text + } + + override init(identifier: NSToolbar.Identifier) { + super.init(identifier: identifier) + delegate = self + if #available(macOS 13.0, *) { + centeredItemIdentifiers.insert(Self.TitleIdentifier) + } else { + centeredItemIdentifier = Self.TitleIdentifier + } + } + + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + guard itemIdentifier == Self.TitleIdentifier else { return nil } + + let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) + toolbarItem.isEnabled = true + toolbarItem.view = self.TitleTextField + return toolbarItem + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [Self.TitleIdentifier] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [Self.TitleIdentifier] + } +} diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index fabbd8dc4..af0d303e4 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -42,8 +42,12 @@ class TerminalWindow: NSWindow { // so we make sure it's the right size/position, and exists. self.toolbarStyle = .unifiedCompact if (self.toolbar == nil) { - self.toolbar = NSToolbar(identifier: "Toolbar") + self.toolbar = TerminalToolbar(identifier: "Toolbar") } + // We directly hide the view containing the title text because if we use the + // `titleVisibility` property for this it prevents the window from hiding the + // tab bar when we get down to a single tab. + self.hideTitleText() } 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. @@ -62,6 +66,21 @@ class TerminalWindow: NSWindow { titlebarContainer.layer?.backgroundColor = color } + // Directly hide the view containing the title text + func hideTitleText() { + guard let toolbarTitleView = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + })?.subviews.first(where: { + $0.className == "NSTitlebarView" + })?.subviews.first(where: { + $0.className == "NSToolbarView" + })?.subviews.first(where: { + $0.className == "NSToolbarTitleView" + }) else { return } + + toolbarTitleView.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) { @@ -74,7 +93,8 @@ class TerminalWindow: NSWindow { // 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 + // If we don't set titleVisibility to hidden here, the toolbar will display a + // "collapsed items" indicator which interferes with the tab bar. titleVisibility = .hidden // Mark the controller for future reference so we can easily find it. Otherwise @@ -90,9 +110,9 @@ class TerminalWindow: NSWindow { } override func removeTitlebarAccessoryViewController(at index: Int) { - let childViewController = titlebarAccessoryViewControllers[index] + let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.TabBarController super.removeTitlebarAccessoryViewController(at: index) - if (childViewController.identifier == Self.TabBarController) { + if (isTabBar) { hideCustomTabBarViews() } } @@ -104,9 +124,6 @@ class TerminalWindow: NSWindow { // Hide the window drag handle. windowDragHandle?.isHidden = true - - // Enable the window title text. - titleVisibility = .visible } private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) { From 4a93181b79e19b6537f0f4399606da6d69f43983 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jan 2024 15:43:35 -0800 Subject: [PATCH 8/9] macos: minor edits --- .../Terminal/TerminalController.swift | 6 ++- .../Features/Terminal/TerminalToolbar.swift | 45 ++++++++++--------- .../Features/Terminal/TerminalWindow.swift | 28 +++++------- 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ab739a88f..37d6a516b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -484,11 +484,13 @@ class TerminalController: NSWindowController, NSWindowDelegate, func titleDidChange(to: String) { guard let window = window as? TerminalWindow else { return } + // Set the main window title window.title = to // Custom toolbar-based title used when titlebar tabs are enabled. - guard let toolbar = window.toolbar as? TerminalToolbar else { return } - toolbar.setTitleText(to) + if let toolbar = window.toolbar as? TerminalToolbar { + toolbar.titleText = to + } } func cellSizeDidChange(to: NSSize) { diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index 685db8b53..3f095294a 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -1,44 +1,49 @@ +import Cocoa + // Custom NSToolbar subclass that displays a centered window title, // in order to accommodate the titlebar tabs feature. - -import Foundation -import Cocoa -import SwiftUI - class TerminalToolbar: NSToolbar, NSToolbarDelegate { - static private let TitleIdentifier = NSToolbarItem.Identifier("TitleText") - private let TitleTextField = NSTextField( - labelWithString: "👻 Ghostty" - ) - - func setTitleText(_ text: String) { - self.TitleTextField.stringValue = text - } + static private let identifier = NSToolbarItem.Identifier("TitleText") + private let titleTextField = NSTextField(labelWithString: "👻 Ghostty") + + var titleText: String { + get { + titleTextField.stringValue + } + + set { + titleTextField.stringValue = newValue + } + } override init(identifier: NSToolbar.Identifier) { super.init(identifier: identifier) + delegate = self + if #available(macOS 13.0, *) { - centeredItemIdentifiers.insert(Self.TitleIdentifier) + centeredItemIdentifiers.insert(Self.identifier) } else { - centeredItemIdentifier = Self.TitleIdentifier + centeredItemIdentifier = Self.identifier } } - func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { - guard itemIdentifier == Self.TitleIdentifier else { return nil } + func toolbar(_ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + guard itemIdentifier == Self.identifier else { return nil } let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) toolbarItem.isEnabled = true - toolbarItem.view = self.TitleTextField + toolbarItem.view = self.titleTextField return toolbarItem } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [Self.TitleIdentifier] + return [Self.identifier] } func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [Self.TitleIdentifier] + return [Self.identifier] } } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index af0d303e4..13ffa8be9 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -44,10 +44,21 @@ class TerminalWindow: NSWindow { if (self.toolbar == nil) { self.toolbar = TerminalToolbar(identifier: "Toolbar") } + // We directly hide the view containing the title text because if we use the // `titleVisibility` property for this it prevents the window from hiding the // tab bar when we get down to a single tab. - self.hideTitleText() + if let toolbarTitleView = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + })?.subviews.first(where: { + $0.className == "NSTitlebarView" + })?.subviews.first(where: { + $0.className == "NSToolbarView" + })?.subviews.first(where: { + $0.className == "NSToolbarTitleView" + }) { + toolbarTitleView.isHidden = true + } } 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. @@ -66,21 +77,6 @@ class TerminalWindow: NSWindow { titlebarContainer.layer?.backgroundColor = color } - // Directly hide the view containing the title text - func hideTitleText() { - guard let toolbarTitleView = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - })?.subviews.first(where: { - $0.className == "NSTitlebarView" - })?.subviews.first(where: { - $0.className == "NSToolbarView" - })?.subviews.first(where: { - $0.className == "NSToolbarTitleView" - }) else { return } - - toolbarTitleView.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) { From 50125c03194588a957965d7b75e59b5ac823aa17 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 31 Jan 2024 15:44:34 -0800 Subject: [PATCH 9/9] macos: whitespace --- .../Features/Terminal/TerminalToolbar.swift | 60 +++++++++---------- macos/Sources/Ghostty/Ghostty.Config.swift | 42 ++++++------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index 3f095294a..5b4760642 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -3,8 +3,8 @@ import Cocoa // 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 = NSTextField(labelWithString: "👻 Ghostty") + static private let identifier = NSToolbarItem.Identifier("TitleText") + private let titleTextField = NSTextField(labelWithString: "👻 Ghostty") var titleText: String { get { @@ -15,35 +15,35 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { titleTextField.stringValue = newValue } } - - override init(identifier: NSToolbar.Identifier) { - super.init(identifier: identifier) + + override init(identifier: NSToolbar.Identifier) { + super.init(identifier: identifier) - delegate = self + delegate = self - if #available(macOS 13.0, *) { - centeredItemIdentifiers.insert(Self.identifier) - } else { - centeredItemIdentifier = Self.identifier - } - } - - func toolbar(_ toolbar: NSToolbar, - itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + if #available(macOS 13.0, *) { + centeredItemIdentifiers.insert(Self.identifier) + } else { + centeredItemIdentifier = Self.identifier + } + } + + func toolbar(_ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { - guard itemIdentifier == Self.identifier else { return nil } - - let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) - toolbarItem.isEnabled = true - toolbarItem.view = self.titleTextField - return toolbarItem - } - - func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [Self.identifier] - } - - func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [Self.identifier] - } + guard itemIdentifier == Self.identifier else { return nil } + + let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) + toolbarItem.isEnabled = true + toolbarItem.view = self.titleTextField + return toolbarItem + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [Self.identifier] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [Self.identifier] + } } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 77c08722c..c26b525eb 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -21,7 +21,7 @@ extension Ghostty { /// Return the errors found while loading the configuration. var errors: [String] { guard let cfg = self.config else { return [] } - + var errors: [String] = []; let errCount = ghostty_config_errors_count(cfg) for i in 0..? = nil @@ -164,7 +164,7 @@ extension Ghostty { guard let ptr = v else { return "" } return String(cString: ptr) } - + var windowDecorations: Bool { guard let config = self.config else { return true } var v = false; @@ -197,39 +197,39 @@ extension Ghostty { _ = ghostty_config_get(config, &v, key, UInt(key.count)) return v } - - var macosTitlebarTabs: Bool { - guard let config = self.config else { return false } - var v = false; - let key = "macos-titlebar-tabs" - _ = ghostty_config_get(config, &v, key, UInt(key.count)) - return v - } + + var macosTitlebarTabs: Bool { + guard let config = self.config else { return false } + var v = false; + let key = "macos-titlebar-tabs" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v + } var backgroundColor: Color { var rgb: UInt32 = 0 let bg_key = "background" if (!ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count))) { - #if os(macOS) +#if os(macOS) return Color(NSColor.windowBackgroundColor) - #elseif os(iOS) +#elseif os(iOS) return Color(UIColor.systemBackground) - #else - #error("unsupported") - #endif +#else +#error("unsupported") +#endif } let red = Double(rgb & 0xff) let green = Double((rgb >> 8) & 0xff) let blue = Double((rgb >> 16) & 0xff) - + return Color( red: red / 255, green: green / 255, blue: blue / 255 ) } - + var backgroundOpacity: Double { guard let config = self.config else { return 1 } var v: Double = 1 @@ -255,11 +255,11 @@ extension Ghostty { let bg_key = "background" _ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count)); } - + let red = Double(rgb & 0xff) let green = Double((rgb >> 8) & 0xff) let blue = Double((rgb >> 16) & 0xff) - + return Color( red: red / 255, green: green / 255,