From 2bf9c777d769e08596c62c7064fd8af086d70229 Mon Sep 17 00:00:00 2001
From: Xiangbao Meng <134181853+bo2themax@users.noreply.github.com>
Date: Fri, 10 Oct 2025 18:21:29 +0200
Subject: [PATCH] Fix `macos-titlebar-tabs` related issues (#9090)
### This pr fixes multiple issues related to `macos-titlebar-tabs`
- [Window title clipping **on
Tahoe**](https://github.com/ghostty-org/ghostty/discussions/9027#discussion-8981483)
- Clipped tab bar **on Tahoe** when creating new ones in fullscreen
> Sequoia doesn't seem to have this issue (at least I didn't reproduce
myself)
- [Title missing **on Tahoe** after dragging a tab into a separate
window](https://github.com/ghostty-org/ghostty/discussions/9027#discussioncomment-14617088)
- [Clipped tab bar **on Tahoe** after dragging from one tab group to
another](https://github.com/ghostty-org/ghostty/discussions/9027#discussioncomment-14626078)
- [Stretched tab bar after switching system
appearance](https://github.com/ghostty-org/ghostty/discussions/9027#discussioncomment-14626918)
### Related issues
I checked all of the open sub-issues in #2349 , most of them should be
fixed in latest main branch (I didn't reproduce)
- [#1692](https://github.com/ghostty-org/ghostty/issues/1692)
@peteschaffner
- [#1945](https://github.com/ghostty-org/ghostty/issues/1945) this one I
reproduce only on Tahoe, and fixed in this pr, @injust
- [#1813](https://github.com/ghostty-org/ghostty/issues/1813) @jacakira
- [#1787](https://github.com/ghostty-org/ghostty/issues/1787)
@roguesherlock
- [#1691](https://github.com/ghostty-org/ghostty/issues/1691) ~**haven't
found a solution yet**(building zig on VM is a pain**)~
> Tried commenting out `isOpaque` check in
`TitlebarTabsVenturaTerminalWindow`, which would fix the issue, but I
see the note there about transparency issue.
>
> After commenting it out, it worked fine for me with blur and opacity
config, so **I might need some more background on this**. Didn't include
this change yet.
>
> [See screenshot
here](https://github.com/user-attachments/assets/eb17642d-b0de-46b2-b42a-19fb38a2c7f0)
### Minor improvements
- Match window title style with `window-title-font-family` and focus
state
---------
Co-authored-by: Mitchell Hashimoto
---
.../TitlebarTabsTahoeTerminalWindow.swift | 74 ++++++++++++++++---
1 file changed, 65 insertions(+), 9 deletions(-)
diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift
index 855d29f52..4e067eddc 100644
--- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift
+++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift
@@ -19,9 +19,21 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
// MARK: NSWindow
+ override var titlebarFont: NSFont? {
+ didSet {
+ DispatchQueue.main.async { [weak self] in
+ guard let self else { return }
+ self.viewModel.titleFont = self.titlebarFont
+ }
+ }
+ }
+
override var title: String {
didSet {
- viewModel.title = title
+ DispatchQueue.main.async { [weak self] in
+ guard let self else { return }
+ self.viewModel.title = self.title
+ }
}
}
@@ -46,17 +58,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
// Check if we have a tab bar and set it up if we have to. See the comment
// on this function to learn why we need to check this here.
setupTabBar()
+
+ viewModel.isMainWindow = true
}
+ override func resignMain() {
+ super.resignMain()
+
+ viewModel.isMainWindow = false
+ }
// 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) {
// If this is the tab bar then we need to set it up for the titlebar
guard isTabBar(childViewController) else {
+ // After dragging a tab into a new window, `hasTabBar` needs to be
+ // updated to properly review window title
+ viewModel.hasTabBar = false
+
super.addTitlebarAccessoryViewController(childViewController)
return
}
+ // When an existing tab is being dragged in to another tab group,
+ // system will also try to add tab bar to this window, so we want to reset observer,
+ // to put tab bar where we want again
+ tabBarObserver = nil
+
// Some setup needs to happen BEFORE it is added, such as layout. If
// we don't do this before the call below, we'll trigger an AppKit
// assertion.
@@ -116,18 +144,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
guard tabBarObserver == nil else { return }
// Find our tab bar. If it doesn't exist we don't do anything.
- guard let tabBar = contentView?.rootView.firstDescendant(withClassName: "NSTabBar") else { return }
+ //
+ // In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`.
+ // In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes;
+ // in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`.
+ // ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205
+ guard let themeFrameView = contentView?.rootView else { return }
+ let titlebarView = if themeFrameView.responds(to: Selector(("titlebarView"))) {
+ themeFrameView.value(forKey: "titlebarView") as? NSView
+ } else {
+ NSView?.none
+ }
+ guard let tabBar = titlebarView?.firstDescendant(withClassName: "NSTabBar") else { return }
// View model updates must happen on their own ticks.
- DispatchQueue.main.async {
- self.viewModel.hasTabBar = true
+ DispatchQueue.main.async { [weak self] in
+ self?.viewModel.hasTabBar = true
}
// Find our clip view
guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return }
guard let accessoryView = clipView.subviews[safe: 0] else { return }
- guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return }
+ guard let titlebarView else { return }
guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return }
+
+ // Make sure tabBar's height won't be stretched
+ guard let newTabButton = titlebarView.firstDescendant(withClassName: "NSTabBarNewTabButton") else { return }
+ tabBar.frame.size.height = newTabButton.frame.width
// The container is the view that we'll constrain our tab bar within.
let container = toolbarView
@@ -209,6 +252,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
case .title:
let item = NSToolbarItem(itemIdentifier: .title)
item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel))
+ // Fix: https://github.com/ghostty-org/ghostty/discussions/9027
+ item.view?.setContentCompressionResistancePriority(.required, for: .horizontal)
item.visibilityPriority = .user
item.isEnabled = true
@@ -225,8 +270,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
// MARK: SwiftUI
class ViewModel: ObservableObject {
+ @Published var titleFont: NSFont?
@Published var title: String = "👻 Ghostty"
@Published var hasTabBar: Bool = false
+ @Published var isMainWindow: Bool = true
}
}
@@ -249,15 +296,24 @@ extension TitlebarTabsTahoeTerminalWindow {
var body: some View {
if !viewModel.hasTabBar {
- Text(title)
- .lineLimit(1)
- .truncationMode(.tail)
+ titleText
} else {
// 1x1.gif strikes again! For real: if we render a zero-sized
// view here then the toolbar just disappears our view. I don't
- // know.
+ // know. This appears fixed in 26.1 Beta but keep it safe for 26.0.
Color.clear.frame(width: 1, height: 1)
}
}
+
+ @ViewBuilder
+ var titleText: some View {
+ Text(title)
+ .font(viewModel.titleFont.flatMap(Font.init(_:)))
+ .foregroundStyle(viewModel.isMainWindow ? .primary : .secondary)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .frame(maxWidth: .greatestFiniteMagnitude, alignment: .center)
+ .opacity(viewModel.hasTabBar ? 0 : 1) // hide when in fullscreen mode, where title bar will appear in the leading area under window buttons
+ }
}
}