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 <img width="843" height="198" alt="image" src="https://github.com/user-attachments/assets/0138c4fa-1a4b-4bab-a415-b32695899ccf" /> --------- Co-authored-by: Mitchell Hashimoto <m@mitchellh.com>pull/9126/head
parent
cbbcf06e6e
commit
2bf9c777d7
|
|
@ -19,9 +19,21 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
|
|
||||||
// MARK: NSWindow
|
// 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 {
|
override var title: String {
|
||||||
didSet {
|
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
|
// 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.
|
// on this function to learn why we need to check this here.
|
||||||
setupTabBar()
|
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 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.
|
// this, detect the tab bar being added, and override its behavior.
|
||||||
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
||||||
// If this is the tab bar then we need to set it up for the titlebar
|
// If this is the tab bar then we need to set it up for the titlebar
|
||||||
guard isTabBar(childViewController) else {
|
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)
|
super.addTitlebarAccessoryViewController(childViewController)
|
||||||
return
|
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
|
// 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
|
// we don't do this before the call below, we'll trigger an AppKit
|
||||||
// assertion.
|
// assertion.
|
||||||
|
|
@ -116,18 +144,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
guard tabBarObserver == nil else { return }
|
guard tabBarObserver == nil else { return }
|
||||||
|
|
||||||
// Find our tab bar. If it doesn't exist we don't do anything.
|
// 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.
|
// View model updates must happen on their own ticks.
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [weak self] in
|
||||||
self.viewModel.hasTabBar = true
|
self?.viewModel.hasTabBar = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find our clip view
|
// Find our clip view
|
||||||
guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return }
|
guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return }
|
||||||
guard let accessoryView = clipView.subviews[safe: 0] 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 }
|
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.
|
// The container is the view that we'll constrain our tab bar within.
|
||||||
let container = toolbarView
|
let container = toolbarView
|
||||||
|
|
@ -209,6 +252,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
case .title:
|
case .title:
|
||||||
let item = NSToolbarItem(itemIdentifier: .title)
|
let item = NSToolbarItem(itemIdentifier: .title)
|
||||||
item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel))
|
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.visibilityPriority = .user
|
||||||
item.isEnabled = true
|
item.isEnabled = true
|
||||||
|
|
||||||
|
|
@ -225,8 +270,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
// MARK: SwiftUI
|
// MARK: SwiftUI
|
||||||
|
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
|
@Published var titleFont: NSFont?
|
||||||
@Published var title: String = "👻 Ghostty"
|
@Published var title: String = "👻 Ghostty"
|
||||||
@Published var hasTabBar: Bool = false
|
@Published var hasTabBar: Bool = false
|
||||||
|
@Published var isMainWindow: Bool = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,15 +296,24 @@ extension TitlebarTabsTahoeTerminalWindow {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if !viewModel.hasTabBar {
|
if !viewModel.hasTabBar {
|
||||||
Text(title)
|
titleText
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
} else {
|
} else {
|
||||||
// 1x1.gif strikes again! For real: if we render a zero-sized
|
// 1x1.gif strikes again! For real: if we render a zero-sized
|
||||||
// view here then the toolbar just disappears our view. I don't
|
// 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)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue