diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 7070a4867..5fd346583 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -1278,6 +1278,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Whenever we resize save our last position and size for the next start. LastWindowPosition.shared.save(window) + + if let window = self.window as? TerminalWindow { + // Expand the title frame to new width. + // This is needed because when the new window size becomes bigger, + // window's title will be clipped again. + window.syncWindowTitleAppearance() + } } func windowDidBecomeMain(_ notification: Notification) { diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 737d146f1..81b6666a1 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -1,4 +1,5 @@ import AppKit +import Combine import SwiftUI import GhosttyKit @@ -277,12 +278,6 @@ class TerminalWindow: NSWindow { /// added. static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") - var hasMoreThanOneTabs: Bool { - /// accessing ``tabGroup?.windows`` here - /// will cause other edge cases, be careful - (tabbedWindows?.count ?? 0) > 1 - } - func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { if childViewController.identifier == nil { // The good case @@ -396,12 +391,9 @@ class TerminalWindow: NSWindow { // Whenever we change the window title we must also update our // tab title if we're using custom fonts. tab.attributedTitle = attributedTitle - /// We also needs to update this here, just in case - /// the value is not what we want - /// - /// Check ``titlebarFont`` down below - /// to see why we need to check `hasMoreThanOneTabs` here - titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs + guard title != oldValue else { return } + + syncWindowTitleAppearance() } } @@ -409,25 +401,28 @@ class TerminalWindow: NSWindow { var titlebarFont: NSFont? { didSet { let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) - - titlebarTextField?.font = font - /// We check `hasMoreThanOneTabs` here because the system - /// may copy this setting to the tab’s text field at some point(e.g. entering/exiting fullscreen), - /// which can cause the title to be vertically misaligned (shifted downward). - /// - /// This behaviour is the opposite of what happens in the title bar’s text field, which is quite odd... - titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs tab.attributedTitle = attributedTitle + + // We need to call this every time the font is set, + // after entering or exiting fullscreen, or other cases, + // AppKit will reset the font. + syncWindowTitleAppearance(font: font) } } - // Find the NSTextField responsible for displaying the titlebar's title. - private var titlebarTextField: NSTextField? { - titlebarContainer? + // Find the array of NSTextField responsible for displaying the titlebar's title. + // In fullscreen mode, there'll be two of them. + private var titlebarTextFields: [NSTextField] { + (titlebarContainer? .firstDescendant(withClassName: "NSTitlebarView")? - .firstDescendant(withClassName: "NSTextField") as? NSTextField + .descendants(withClassName: "NSTextField") + .compactMap { $0 as? NSTextField } ?? []) + .filter({ $0.superview?.className == "NSTitlebarView" }) } + // Cancellables for the frame change of the text fields in the titlebar. + private var titlebarTextFieldFrameCancellables = Set() + // Return a styled representation of our title property. var attributedTitle: NSAttributedString? { guard let titlebarFont = titlebarFont else { return nil } @@ -463,6 +458,65 @@ class TerminalWindow: NSWindow { // MARK: Positioning And Styling + /// Update titlebarTextField's size and font + func syncWindowTitleAppearance(font: NSFont? = nil) { + titlebarTextFields.forEach { field in + if let font { + field.font = font + } + } + + titlebarTextFieldFrameCancellables.removeAll() + + // macOS 15 doesn't seem to need to adjust the frame. + // + // When using custom font, we always expand the frame to + // show the text properly. + // + // AppKit will relayout the frame when the font changes, that's + // why we need this hack in the first place. + guard #available(macOS 26.0, *), titlebarFont != nil else { + return + } + + titlebarTextFields.forEach { field in + setWindowTitleFrameSize(field) + } + + // We need to observe changes to make the frame correct in some cases: + // + // 1. Entering fullscreen mode. + // > Updating the frame when it's hidden doesn't seem to work. + // 2. Exiting fullscreen mode. + // 3. Resizing the window. + // 3. Maybe more... + + titlebarTextFields.forEach { field in + field.postsFrameChangedNotifications = true + NotificationCenter.default + .publisher(for: NSView.frameDidChangeNotification, object: field) + .compactMap { $0.object as? NSTextField } + .sink { [weak self] in + self?.setWindowTitleFrameSize($0) + } + .store(in: &titlebarTextFieldFrameCancellables) + } + } + + private func setWindowTitleFrameSize(_ textfield: NSTextField) { + guard let superview = textfield.superview else { return } + + // Button group size estimate: 92. + // Make the available space a little bit smaller. + let fittingSize = textfield.sizeThatFits(superview.bounds.insetBy(dx: 92/2 + 6, dy: 0).size) + // We make the frame a little bit higher so fixed-width fonts can + // align center vertically as well. + let properSize = CGSize(width: fittingSize.width, height: fittingSize.height * 1.1) + + guard textfield.frame.size != properSize else { return } + textfield.frame.size = properSize + } + /// This is called by the controller when there is a need to reset the window appearance. func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // If our window is not visible, then we do nothing. Some things such as blurring