From cf6017e777cda0e0c131b616f408c9a81644b5d7 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Thu, 28 Mar 2024 10:48:05 +0100 Subject: [PATCH 1/8] Revert "Revert "Merge pull request #1550 from peteschaffner/titlebar-unzoom-button"" This reverts commit 7f59d844c098f65158b4c5674f53371e0c4a4f1a. --- .../ResetZoom.imageset/Contents.json | 15 + .../ResetZoom.imageset/ResetZoom.pdf | Bin 0 -> 4795 bytes .../Terminal/TerminalController.swift | 66 ++- .../Features/Terminal/TerminalToolbar.swift | 69 +-- .../Features/Terminal/TerminalView.swift | 12 +- .../Features/Terminal/TerminalWindow.swift | 405 +++++++++++------- 6 files changed, 350 insertions(+), 217 deletions(-) create mode 100644 macos/Assets.xcassets/ResetZoom.imageset/Contents.json create mode 100644 macos/Assets.xcassets/ResetZoom.imageset/ResetZoom.pdf diff --git a/macos/Assets.xcassets/ResetZoom.imageset/Contents.json b/macos/Assets.xcassets/ResetZoom.imageset/Contents.json new file mode 100644 index 000000000..b5bca19ac --- /dev/null +++ b/macos/Assets.xcassets/ResetZoom.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "ResetZoom.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/macos/Assets.xcassets/ResetZoom.imageset/ResetZoom.pdf b/macos/Assets.xcassets/ResetZoom.imageset/ResetZoom.pdf new file mode 100644 index 0000000000000000000000000000000000000000..fa36790fc94eb8396b6ea25438d34198f23fedb2 GIT binary patch literal 4795 zcmai&cTf{++lOgN5djtH>WUPpDTFRP5D=6mNT?o z%{xr{b=QuPBwL}#5bix~JGa+bi<;b_`q?viex0SP;~?*AAVlS8ubcOIIgtmG6bEWTz>;c}O`DgBn< z`)t#JDu@MOmK+~KK94lUHK4B*f!Ef!-cG3P)%AB>{*YE0BO>)V(&?UQ?YT|HZ_b%H zUecO+0guR#FHMvd8BxLJiy*1vlew)wL{bFwg18Md2TSb|OyVSbBTckA)wx5+Tv1I{ zKonC4*6npIXFLj#lQ8y7t0)0>+3}`rN}gn~saNc=H`eJVF_RCyCDvU@dlq;H{?SlQ z-gY-O2@lT&>qz_Wj1F^f5f{CllcUAuHaiFhezrO@0mh5UxJZ50zJu!RH~j^V&=Wxl zswb@idOS-9B_41V_-hs1D7C8MJSAV!{>al;&C-nBBkJ)sTybpEeYH@oEJaXC#L5}d zD`wfuw&8p~hFbFS`+){{#IbfQjJ4S7%`9y30}@N>UYYZVDfyw!TQ#cPIcr}_^~N}q zY8#P!T_XH!v$mR9aowql5s6qumpaWJYQ%984*j8E_5n6Rx50z+S_hmtSt#uR@6+Jm zsMm?`m$x=D?)*gao<)ubIo1`iKjoYwpH9fwGy%8Q3DxUM=dlkJ85Bsq3TbL78xVwC z)J$X^ON=RB_(*nj22l4Dd)Pj9_dWDhZnJ*lSY(B}@zB=I$Z#4@jZo~S7ai(%?a&oV zpNtm;;s7RPW{qZftf|5seXbYF*(Jzrrsr2ngeTpg&={l^Wr;4FT(N z(;+2|W->Z%Ctl8q^Fclzqb$G$RT%eD8hDxLK3xDkJ_Sh?{c zV`oEuZ;Fz+u_Ro3YGZpn!9PFgS}pTv{h>B8ZeI7W_^>v=wzu+-`dk=|78AzJ?oUWz z-0Y6f2KfzfH}1G&0FVL7;nx|DaU%dw01OxbAR~+i&J%Bo@c>|d1R6Lu0>+Kt0UROH z=!kuP&O2KF3z-IZoUJj209XJZ1GpAo1%NbgE;zigJIWRV95GeH0|H3@IpDV$GQZ8Z z4oCqYwIlSpp$HfN1jl$`Z81g&_5Z>bDg%N3Z#@4_7A*kQ$Qu$s!?$9=l@1T*SDSe6<4x5awP(`Pyww`sW8s9DUY z>n82(=@2_`n!X=orIeHc?dI~WZ<2v_q*3u&10j!r!+;omSq*{XR7{%vGFSYCIA>_$ zjmw+3p@($s8wn!NPzaPGi!r;Hf2#3?%U1rZ7^y8eNoYEY2ie8_4wgYA8H}e2PUaBl zvMiGYKRe{yqsNeM=%iB3Y+vr}-AU7<0<)*sbj}H7xr9SC*}1=kf2W~hh;NjChi8f0 zm3Pdb^2u|Uj-U(ILFb(sOvcW9ZMEN9D>vRnL&V}6u1!Al(W7isFh2rw2EM>0YEla# zT~i!mmP`1SmyrXrj=QzP6X*Sn50qMc_f6J>mM9r�=zw%Th6j%t5s!VE*J>c};=| zRfX$u)tSj+{S|=f`!TAD@wD~xp)S_3jbE6BPed*)xp+`#I!lUumEF(0k87_DmETV3 z__lAVn8&FSbV=u|6h?CmP*k(JNs*}flXloEp%dF#E3;Akd_%)s} zINXsGawW*@v7{YSa6tcL0GEs~J-2_lGrOaw4V7Pryg`)?eJf6hO5+l(fSZahjo+dL z8-%H~=AC0n%nOfbsPE6Yy$dHSBittS)cB2Ox0s{t@PgUm+&L4$F7-60AJY?_GQol* z!8GExPVxmxxzmmVRKbDTY_y5AlG-#_Rk~Md%0pC4YLFpnuLn-cQSq`i}tY_yM(F@Tp{)Uew7UsyBhdWr}?)=bYqSr4kRZc^}81+R8xNt!2i*y9QT(Y$iZ`>Y{TDe=bd- zX6j>>T;8`)+A+7MX^)3xp^~{cnz^UKkiYoe0Rb0*8*Drba|{Q; zr{2R1g9`Ks{Gn`j8ZFTxFCPu5z0=_nx*o@_6Dq)IBp{Mf5p+YxiQh^{Nr-$gu1&!X zf#*W1TI-hzWCAvVtg$Kv@t1|z^{?`2st;e6zxr17j_MtaXGSLr-1^L(Cs(JN>yzPr zSN%YUIH{!dq^Y)&xS|;6gvF%0g8K1;N#02|l9|Fb7Xz;T%==MAV_2_RZyc0UW}2ln zS@B*eL`O3OmO||{prtLpX>L5orIiFPQy<7{gOpdlzfVJ z1|{7Fb~ybbY@w_=+_!4aX|rKLX(5g$m8XV>o5zGF3Dzvlo$8j_mO7I<2WzQtG(Yj0 z&V0sv?zJ^IA>~`=4ST`zNcpUi(&c=KZspvuBBc`Bf^sv5oa^OXXRKl+Eo9XyEh~+^ z)%A5jW+0mvhAUahhM6UqxwVK|i-7jJkHE;SNVO;R1z8bUwWthK&ZO<7=P^ayqB-U{ zip_GiZ*E7dDNk7lm4&D4#Og$~T{9l)8^3X@no>bwcAj+AAcvbeFtqWA@*2=bVjYsEs@Uc*RZsqrIKX$HJY%Xfe zY6tTphAt`iRd6>vsnIIjR;N_wtj-jTzS`^0p08}a2Td2oKaS54kt*yi{D2Lv*s17M z7eqyriCIZ036nt$upF>Pbvw5Io5`7M)$SO*%N|I3Qu3ppdYjrrfhU3wkSEHWUX42- zFq!R!ou_6urc?UI@Eam4E<3r{mq%BBxIn}a`f$WZ>#$1$<3O@ zXji~hdw8`&t0M}-JK+{F-e>sKu&m~mWBJN#v-(?g!%4D4mj#PUlgn9`I)}CfBr)5LJ&`M>MVKTZsl_NNXzJ8?SyMU zFLe}c6TR|zcD{E3t=k69C2Foss85r{4V1;~h3Z;|0Gj~#vi4dG1rdUH*782uvtO}q zP_{reUgmvNZ1c;h+by_*&zl-|&lJrYNaeiUp_98PzxJhN=fX+`^OfN{5q{6-g%2{f z-h4EVFwYwJG!XtKR|b1m@9xBWU~l@lu5%J9PY&;G5VulN%iA4vDKTAV)1Js@di#G@ zZw^_D4Cf3}xJgj1u9)sgFD)w_?+8kIVZHViUpPwM|6!6;UYTd32c0OB;E7@0(=J-9zh7O7ni> z2i^3Atkx=>zH$eS2ewO7*}_cvq8ZOsN)GyW2JH*U8%s|IZ&Au0Iz)$7X@CwBc3 z9HSIc#I!2377!%}3UQY>=KFG^X|;ox)H@YYEAsQrc0zYiroZ<-*#X_wxa!=!n4)rZ zyr4#YZ(z%H&hG1UuQzrjFF#_us77Tg=pbv~cB3U`eo22vKOr+p#rq!PkMBN0uJ*GN z+rjJm|90ggRXuWB3jm~}p`niQz}Nvtu4)8W{iVN0zWkT|{;ks<0O%UV4vSL9`2ZGR z01QB200j2Shdtd000aQ(V(mNt3*gAcjR33PTv+yB12s?tlnc(`4^j8{)6oAGaJk#W zm;C*RK6s2h6Bv+Z0{`~_WTd5~qyc;2&kO>Ul>&aP0&ah0Fo=xYKQk~G3jJpWk&^x& zy;A?mATTK8pS>_BO#a`Q{BQRq;89o?4F1 1 - + for (index, window) in windows.enumerated().prefix(9) { let action = "goto_tab:\(index + 1)" - guard let equiv = ghostty.config.keyEquivalent(for: action) else { - continue + + if let equiv = ghostty.config.keyEquivalent(for: action) { + window.keyEquivalent = "\(equiv)" } - - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.labelFont(ofSize: 0), - .foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - let attributedString = NSAttributedString(string: " \(equiv) ", attributes: attributes) - let text = NSTextField(labelWithAttributedString: attributedString) - text.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) - text.postsFrameChangedNotifications = true - window.tab.accessoryView = text } } - + private func fixTabBar() { // We do this to make sure that the tab bar will always re-composite. If we don't, // then the it will "drag" pieces of the background with it when a transparent @@ -200,7 +191,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, leaf.surface.focusDidChange(focused) } } - + //MARK: - NSWindowController override func windowWillLoad() { @@ -255,6 +246,16 @@ class TerminalController: NSWindowController, NSWindowDelegate, // when cascading. window.center() + // Set the background color of the window. We only do this if the lum is + // over 0.1 to prevent: https://github.com/mitchellh/ghostty/issues/1549 + let bgColor = NSColor(ghostty.config.backgroundColor) + if (bgColor.luminance > 0.1) { + window.backgroundColor = bgColor + } + + // This makes sure our titlebar renders correctly when there is a transparent background + window.titlebarOpacity = ghostty.config.backgroundOpacity + // Handle titlebar tabs config option. Something about what we do while setting up the // titlebar tabs interferes with the window restore process unless window.tabbingMode // is set to .preferred, so we set it, and switch back to automatic as soon as we can. @@ -265,20 +266,8 @@ class TerminalController: NSWindowController, NSWindowDelegate, DispatchQueue.main.async { window.tabbingMode = .automatic } - - // Set the background color of the window - window.backgroundColor = NSColor(ghostty.config.backgroundColor) - - // Set a custom background on the titlebar - this is required for when - // titlebar tabs are used in conjunction with a transparent background. - window.setTitlebarBackground( - window - .backgroundColor - .withAlphaComponent(ghostty.config.backgroundOpacity) - .cgColor - ) } - + // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, @@ -313,7 +302,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, guard let surface = self.focusedSurface?.surface else { return } ghostty.newTab(surface: surface) } - + //MARK: - NSWindowDelegate // This is called when performClose is called on a window (NOT when close() @@ -393,7 +382,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, } } } - + // Called when the window will be encoded. We handle the data encoding here in the // window controller. func window(_ window: NSWindow, willEncodeRestorableState state: NSCoder) { @@ -594,7 +583,12 @@ class TerminalController: NSWindowController, NSWindowDelegate, // we want to invalidate our state. invalidateRestorableState() } - + + func zoomStateDidChange(to: Bool) { + guard let window = window as? TerminalWindow else { return } + window.surfaceIsZoomed = to + } + //MARK: - Clipboard Confirmation func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, _ request: Ghostty.ClipboardRequest) { diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index b0857cb24..88a093d87 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -3,7 +3,6 @@ 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 = CenteredDynamicLabel(labelWithString: "👻 Ghostty") var titleText: String { @@ -15,56 +14,61 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { titleTextField.stringValue = newValue } } - + override init(identifier: NSToolbar.Identifier) { super.init(identifier: identifier) delegate = self if #available(macOS 13.0, *) { - centeredItemIdentifiers.insert(Self.identifier) + centeredItemIdentifiers.insert(.titleText) } else { - centeredItemIdentifier = Self.identifier + centeredItemIdentifier = .titleText } } func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { - guard itemIdentifier == Self.identifier else { - return NSToolbarItem(itemIdentifier: itemIdentifier) + var item: NSToolbarItem + + switch itemIdentifier { + case .titleText: + item = NSToolbarItem(itemIdentifier: .titleText) + item.view = self.titleTextField + item.visibilityPriority = .user + + // NSToolbarItem.minSize and NSToolbarItem.maxSize are deprecated, and make big ugly + // warnings in Xcode when you use them, but I cannot for the life of me figure out + // how to get this to work with constraints. The behavior isn't the same, instead of + // shrinking the item and clipping the subview, it hides the item as soon as the + // intrinsic size of the subview gets too big for the toolbar width, regardless of + // whether I have constraints set on its width, height, or both :/ + // + // If someone can fix this so we don't have to use deprecated properties: Please do. + item.minSize = NSSize(width: 32, height: 1) + item.maxSize = NSSize(width: 1024, height: self.titleTextField.intrinsicContentSize.height) + + item.isEnabled = true + case .resetZoom: + item = NSToolbarItem(itemIdentifier: .resetZoom) + default: + item = NSToolbarItem(itemIdentifier: itemIdentifier) } - - let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) - toolbarItem.view = self.titleTextField - toolbarItem.visibilityPriority = .user - - // NSToolbarItem.minSize and NSToolbarItem.maxSize are deprecated, and make big ugly - // warnings in Xcode when you use them, but I cannot for the life of me figure out - // how to get this to work with constraints. The behavior isn't the same, instead of - // shrinking the item and clipping the subview, it hides the item as soon as the - // intrinsic size of the subview gets too big for the toolbar width, regardless of - // whether I have constraints set on its width, height, or both :/ - // - // If someone can fix this so we don't have to use deprecated properties: Please do. - toolbarItem.minSize = NSSize(width: 32, height: 1) - toolbarItem.maxSize = NSSize(width: 1024, height: self.titleTextField.intrinsicContentSize.height) - - toolbarItem.isEnabled = true - - return toolbarItem + + return item } func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [Self.identifier, .space] + return [.titleText, .flexibleSpace, .space, .resetZoom] } func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { // These space items are here to ensure that the title remains centered when it starts - // getting smaller than the max size so starts clipping. Lucky for us, three of the - // built-in spacers seems to exactly match the space on the left that's reserved for - // the window buttons. - return [Self.identifier, .space, .space, .space] + // getting smaller than the max size so starts clipping. Lucky for us, two of the + // built-in spacers plus the un-zoom button item seems to exactly match the space + // on the left that's reserved for the window buttons. + return [.titleText, .flexibleSpace, .space, .space, .resetZoom] } } @@ -83,3 +87,8 @@ fileprivate class CenteredDynamicLabel: NSTextField { needsLayout = true } } + +extension NSToolbarItem.Identifier { + static let resetZoom = NSToolbarItem.Identifier("ResetZoom") + static let titleText = NSToolbarItem.Identifier("TitleText") +} diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index d0766c7ab..8e1f0dbdd 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -17,6 +17,8 @@ protocol TerminalViewDelegate: AnyObject { /// The surface tree did change in some way, i.e. a split was added, removed, etc. This is /// not called initially. func surfaceTreeDidChange() + + func zoomStateDidChange(to: Bool) } // Default all the functions so they're optional @@ -24,6 +26,7 @@ extension TerminalViewDelegate { func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {} func titleDidChange(to: String) {} func cellSizeDidChange(to: NSSize) {} + func zoomStateDidChange(to: Bool) {} } /// The view model is a required implementation for TerminalView callers. This contains @@ -64,12 +67,6 @@ struct TerminalView: View { } } - if let zoomedSplit = zoomedSplit { - if zoomedSplit { - title = "🔍 " + title - } - } - return title } @@ -107,6 +104,9 @@ struct TerminalView: View { // in the hash value. self.delegate?.surfaceTreeDidChange() } + .onChange(of: zoomedSplit) { newValue in + self.delegate?.zoomStateDidChange(to: newValue ?? false) + } } } } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 5fa06cfb0..c8e194644 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -1,15 +1,116 @@ import Cocoa class TerminalWindow: NSWindow { + @objc dynamic var surfaceIsZoomed: Bool = false + @objc dynamic var keyEquivalent: String = "" + + var titlebarOpacity: CGFloat = 1 { + didSet { + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }) else { return } + + titlebarContainer.wantsLayer = true + titlebarContainer.layer?.backgroundColor = backgroundColor.withAlphaComponent(titlebarOpacity).cgColor + } + } + + private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() + + private lazy var resetZoomTabButton: NSButton = { + let button = generateResetZoomButton() + button.action = #selector(selectTabAndZoom(_:)) + return button + }() + + private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } + + let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) + let view = NSView(frame: NSRect(origin: .zero, size: size)) + + let button = generateResetZoomButton() + button.frame.origin.x = size.width/2 - button.bounds.width/2 + button.frame.origin.y = size.height/2 - button.bounds.height/2 + view.addSubview(button) + + let titlebarAccessoryViewController = NSTitlebarAccessoryViewController() + titlebarAccessoryViewController.view = view + titlebarAccessoryViewController.layoutAttribute = .right + + return titlebarAccessoryViewController + }() + + private lazy var keyEquivalentLabel: NSTextField = { + let label = NSTextField(labelWithAttributedString: NSAttributedString()) + label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) + label.postsFrameChangedNotifications = true + + return label + }() + + private lazy var bindings = [ + observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in + guard let tabGroup = self?.tabGroup else { return } + + self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed + self?.updateResetZoomTitlebarButtonVisibility() + }, + + observe(\.keyEquivalent, options: [.initial, .new]) { [weak self] window, _ in + let attributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), + .foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + ] + let attributedString = NSAttributedString(string: " \(window.keyEquivalent) ", attributes: attributes) + + self?.keyEquivalentLabel.attributedStringValue = attributedString + }, + ] + // 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: - Lifecycle + + override func awakeFromNib() { + super.awakeFromNib() + + _ = bindings + + // By hiding the visual effect view, we allow the window's (or titlebar's in this case) + // background color to show through. If we were to set `titlebarAppearsTransparent` to true + // the selected tab would look fine, but the unselected ones and new tab button backgrounds + // would be an opaque color. When the titlebar isn't transparent, however, the system applies + // a compositing effect to the unselected tab backgrounds, which makes them blend with the + // titlebar's/window's background. + if let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }), let effectView = titlebarContainer.descendants(withClassName: "NSVisualEffectView").first { + effectView.isHidden = true + } + + // Create the tab accessory view that houses the key-equivalent label and optional un-zoom button + let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) + stackView.setHuggingPriority(.defaultHigh, for: .horizontal) + stackView.spacing = 3 + tab.accessoryView = stackView + + if titlebarTabs { + generateToolbar() + } + } + + deinit { + bindings.forEach() { $0.invalidate() } + } + // MARK: - NSWindow override func becomeKey() { - // This is required because the removeTitlebarAccessoryViewControlle hook does not + // This is required because the removeTitlebarAccessoryViewController 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() @@ -17,32 +118,177 @@ class TerminalWindow: NSWindow { super.becomeKey() - if titlebarTabs { - updateNewTabButtonOpacity() - } + updateNewTabButtonOpacity() + resetZoomTabButton.contentTintColor = .controlAccentColor + resetZoomToolbarButton.contentTintColor = .controlAccentColor } override func resignKey() { super.resignKey() - if titlebarTabs { - updateNewTabButtonOpacity() - } + updateNewTabButtonOpacity() + resetZoomTabButton.contentTintColor = .secondaryLabelColor + resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor } + override func update() { + super.update() + + updateResetZoomTitlebarButtonVisibility() + + titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none + + // This is called when we open, close, switch, and reorder tabs, at which point we determine if the + // first tab in the tab bar is selected. If it is, we make the `windowButtonsBackdrop` color the same + // as that of the active tab (i.e. the titlebar's background color), otherwise we make it the same + // color as the background of unselected tabs. + if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self), titlebarTabs { + windowButtonsBackdrop?.isHighlighted = index == 0 + } + + // Color the new tab button's image to match the color of the tab title/keyboard shortcut labels, + // just as it does in the stock tab bar. + updateNewTabButtonOpacity() + + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }) else { return } + guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } + guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { + $0 as? NSImageView != nil + }) as? NSImageView else { return } + guard let newTabButtonImage = newTabButtonImageView.image else { return } + + let isLightTheme = backgroundColor.isLightColor + + if newTabButtonImageLayer == nil { + let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) + let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in + newTabButtonImage.draw(in: rect) + fillColor.setFill() + rect.fill(using: .sourceAtop) + return true + } + let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! + imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) + imageLayer.contentsGravity = .resizeAspect + imageLayer.contents = newImage + imageLayer.opacity = 0.5 + + newTabButtonImageLayer = imageLayer + } + + newTabButtonImageView.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() + newTabButtonImageView.layer?.addSublayer(newTabButtonImageLayer!) + newTabButtonImageView.image = nil + // When we nil out the original image, the image view's frame resizes and repositions + // slightly, so we need to reset it to make sure our new image doesn't shift quickly. + newTabButtonImageView.frame = newTabButton.bounds + } + + // MARK: - + + private var newTabButtonImageLayer: VibrantLayer? = nil + + // Since we are coloring the new tab button's image, it doesn't respond to the + // window's key status changes in terms of becoming less prominent visually, + // so we need to do it manually. + private func updateNewTabButtonOpacity() { + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }) else { return } + guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } + guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { + $0 as? NSImageView != nil + }) as? NSImageView else { return } + + newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5 + } + + private func updateResetZoomTitlebarButtonVisibility() { + guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return } + + let isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed + + if titlebarTabs { + resetZoomToolbarButton.isHidden = isHidden + + for (index, vc) in titlebarAccessoryViewControllers.enumerated() { + guard vc == resetZoomTitlebarAccessoryViewController else { return } + removeTitlebarAccessoryViewController(at: index) + } + } else { + if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) { + addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController) + } + resetZoomTitlebarAccessoryViewController.view.isHidden = isHidden + } + } + + // We have to regenerate a toolbar when the titlebar tabs setting changes since our + // custom toolbar conditionally generates the items based on this setting. I tried to + // invalidate the toolbar items and force a refresh, but as far as I can tell that + // isn't possible. + private func generateToolbar() { + let terminalToolbar = TerminalToolbar(identifier: "Toolbar") + + toolbar = terminalToolbar + toolbarStyle = .unifiedCompact + if let resetZoomItem = terminalToolbar.items.first(where: { $0.itemIdentifier == .resetZoom }) { + resetZoomItem.view = resetZoomToolbarButton + resetZoomItem.view?.translatesAutoresizingMaskIntoConstraints = false + resetZoomItem.view?.widthAnchor.constraint(equalToConstant: 22).isActive = true + resetZoomItem.view?.heightAnchor.constraint(equalToConstant: 20).isActive = true + } + updateResetZoomTitlebarButtonVisibility() + } + + private func generateResetZoomButton() -> NSButton { + let button = NSButton() + button.target = nil + button.action = #selector(TerminalController.splitZoom(_:)) + button.isBordered = false + button.allowsExpansionToolTips = true + button.toolTip = "Reset Zoom" + button.contentTintColor = .controlAccentColor + button.state = .on + button.image = NSImage(named:"ResetZoom") + button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) + button.translatesAutoresizingMaskIntoConstraints = false + button.widthAnchor.constraint(equalToConstant: 20).isActive = true + button.heightAnchor.constraint(equalToConstant: 20).isActive = true + + return button + } + + @objc private func selectTabAndZoom(_ sender: NSButton) { + guard let tabGroup else { return } + + guard let associatedWindow = tabGroup.windows.first(where: { + guard let accessoryView = $0.tab.accessoryView else { return false } + return accessoryView.subviews.contains(sender) + }), + let windowController = associatedWindow.windowController as? TerminalController + else { return } + + tabGroup.selectedWindow = associatedWindow + windowController.splitZoom(self) + } + // MARK: - Titlebar Tabs // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { didSet { - changedTitlebarTabs(to: titlebarTabs) + self.titleVisibility = titlebarTabs ? .hidden : .visible + if titlebarTabs { + generateToolbar() + } } } private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil private var windowDragHandle: WindowDragView? = nil - private var storedTitlebarBackgroundColor: CGColor? = nil - private var newTabButtonImageLayer: VibrantLayer? = nil // The tab bar controller ID from macOS static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController") @@ -66,72 +312,6 @@ class TerminalWindow: NSWindow { } } - /// This is called by titlebarTabs changing so that we can setup the rest of our window - private func changedTitlebarTabs(to newValue: Bool) { - if (newValue) { - // By hiding the visual effect view, we allow the window's (or titlebar's in this case) - // background color to show through. If we were to set `titlebarAppearsTransparent` to true - // the selected tab would look fine, but the unselected ones and new tab button backgrounds - // would be an opaque color. When the titlebar isn't transparent, however, the system applies - // a compositing effect to the unselected tab backgrounds, which makes them blend with the - // titlebar's/window's background. - if let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }), let effectView = titlebarContainer.descendants(withClassName: "NSVisualEffectView").first { - effectView.isHidden = true - } - - self.titlebarSeparatorStyle = .none - - // 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 = TerminalToolbar(identifier: "Toolbar") - } - - // Set a custom background on the titlebar - this is required for when - // titlebar tabs is used in conjunction with a transparent background. - self.restoreTitlebarBackground() - - // Reset the new tab button image so that we are sure to generate a fresh - // one, tinted appropriately for the given theme. - self.newTabButtonImageLayer = nil - - // We have to wait before setting the titleVisibility or else it prevents - // the window from hiding the tab bar when we get down to a single tab. - DispatchQueue.main.async { - self.titleVisibility = .hidden - } - } 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 - - // Reset the appearance to whatever our app global value is - self.appearance = nil - } - } - - // Assign a background color to the titlebar area. - func setTitlebarBackground(_ color: CGColor) { - storedTitlebarBackgroundColor = color - - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - - titlebarContainer.wantsLayer = true - titlebarContainer.layer?.backgroundColor = color - } - - // Make sure the titlebar has the assigned background color. - private func restoreTitlebarBackground() { - guard let color = storedTitlebarBackgroundColor else { return } - setTitlebarBackground(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) { @@ -214,73 +394,6 @@ class TerminalWindow: NSWindow { } } - override func update() { - super.update() - - guard titlebarTabs else { return } - - // This is called when we open, close, switch, and reorder tabs, at which point we determine if the - // first tab in the tab bar is selected. If it is, we make the `windowButtonsBackdrop` color the same - // as that of the active tab (i.e. the titlebar's background color), otherwise we make it the same - // color as the background of unselected tabs. - if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self) { - windowButtonsBackdrop?.isHighlighted = index == 0 - } - - // Color the new tab button's image to match the color of the tab title/keyboard shortcut labels, - // just as it does in the stock tab bar. - updateNewTabButtonOpacity() - - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } - guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { - $0 as? NSImageView != nil - }) as? NSImageView else { return } - guard let newTabButtonImage = newTabButtonImageView.image else { return } - guard let storedTitlebarBackgroundColor, let isLightTheme = NSColor(cgColor: storedTitlebarBackgroundColor)?.isLightColor else { return } - - if newTabButtonImageLayer == nil { - let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) - let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in - newTabButtonImage.draw(in: rect) - fillColor.setFill() - rect.fill(using: .sourceAtop) - return true - } - let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! - imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) - imageLayer.contentsGravity = .resizeAspect - imageLayer.contents = newImage - imageLayer.opacity = 0.5 - - newTabButtonImageLayer = imageLayer - } - - newTabButtonImageView.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() - newTabButtonImageView.layer?.addSublayer(newTabButtonImageLayer!) - newTabButtonImageView.image = nil - // When we nil out the original image, the image view's frame resizes and repositions - // slightly, so we need to reset it to make sure our new image doesn't shift quickly. - newTabButtonImageView.frame = newTabButton.bounds - } - - // Since we are coloring the new tab button's image, it doesn't respond to the - // window's key status changes in terms of becoming less prominent visually, - // so we need to do it manually. - private func updateNewTabButtonOpacity() { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } - guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { - $0 as? NSImageView != nil - }) as? NSImageView else { return } - - newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5 - } - private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) { // If we already made the view, just make sure it's unhidden and correctly placed as a subview. if let view = windowButtonsBackdrop { @@ -294,7 +407,9 @@ class TerminalWindow: NSWindow { return } - let view = WindowButtonsBackdropView(backgroundColor: storedTitlebarBackgroundColor ?? NSColor.windowBackgroundColor.cgColor) + let backdropColor = backgroundColor.withAlphaComponent(titlebarOpacity).usingColorSpace(colorSpace!)!.cgColor + + let view = WindowButtonsBackdropView(backgroundColor: backdropColor) view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop") titlebarView.addSubview(view) From bbe35ee02eec292a604bbe2946c723f9cda47a57 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Thu, 28 Mar 2024 10:51:00 +0100 Subject: [PATCH 2/8] Revert luminance check from commit f7129880f5c4 This fixes the issues reporting wrong toolbar colors: https://github.com/mitchellh/ghostty/pull/1550#issuecomment-2021538747 --- macos/Sources/Features/Terminal/TerminalController.swift | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 07e679771..863e9f88d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -246,12 +246,8 @@ class TerminalController: NSWindowController, NSWindowDelegate, // when cascading. window.center() - // Set the background color of the window. We only do this if the lum is - // over 0.1 to prevent: https://github.com/mitchellh/ghostty/issues/1549 - let bgColor = NSColor(ghostty.config.backgroundColor) - if (bgColor.luminance > 0.1) { - window.backgroundColor = bgColor - } + // Set the background color of the window + window.backgroundColor = NSColor(ghostty.config.backgroundColor) // This makes sure our titlebar renders correctly when there is a transparent background window.titlebarOpacity = ghostty.config.backgroundOpacity From f086bff65125c49dd11ebaa4cc75fb2bc0f5f41e Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Tue, 2 Apr 2024 22:28:34 +0200 Subject: [PATCH 3/8] Make things work with really dark backgrounds This fixes issue #1549 --- .../Features/Terminal/TerminalWindow.swift | 185 ++++++++++++------ macos/Sources/Helpers/NSView+Extension.swift | 13 ++ 2 files changed, 133 insertions(+), 65 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index c8e194644..73a16b54b 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -80,18 +80,6 @@ class TerminalWindow: NSWindow { _ = bindings - // By hiding the visual effect view, we allow the window's (or titlebar's in this case) - // background color to show through. If we were to set `titlebarAppearsTransparent` to true - // the selected tab would look fine, but the unselected ones and new tab button backgrounds - // would be an opaque color. When the titlebar isn't transparent, however, the system applies - // a compositing effect to the unselected tab backgrounds, which makes them blend with the - // titlebar's/window's background. - if let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }), let effectView = titlebarContainer.descendants(withClassName: "NSVisualEffectView").first { - effectView.isHidden = true - } - // Create the tab accessory view that houses the key-equivalent label and optional un-zoom button let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) stackView.setHuggingPriority(.defaultHigh, for: .horizontal) @@ -131,59 +119,57 @@ class TerminalWindow: NSWindow { resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor } + override func layoutIfNeeded() { + super.layoutIfNeeded() + + guard titlebarTabs else { return } + + // We need to be aggressive with this, and it has to be done as well in `update`, + // otherwise things can get out of sync and flickering can occur. + updateTabsForVeryDarkBackgrounds() + } + + // We only need to set this once, but need to do it after the window has been created in order + // to determine if the theme is using a very dark background, in which case we don't want to + // remove the effect view if the default tab bar is being used since the effect created in + // `updateTabsForVeryDarkBackgrounds`. + private var effectViewIsHidden = false + override func update() { super.update() - updateResetZoomTitlebarButtonVisibility() + titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none - titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none + if !effectViewIsHidden { + // By hiding the visual effect view, we allow the window's (or titlebar's in this case) + // background color to show through. If we were to set `titlebarAppearsTransparent` to true + // the selected tab would look fine, but the unselected ones and new tab button backgrounds + // would be an opaque color. When the titlebar isn't transparent, however, the system applies + // a compositing effect to the unselected tab backgrounds, which makes them blend with the + // titlebar's/window's background. + if let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }), let effectView = titlebarContainer.descendants(withClassName: "NSVisualEffectView").first { + effectView.isHidden = titlebarTabs || !titlebarTabs && !hasVeryDarkBackground + } - // This is called when we open, close, switch, and reorder tabs, at which point we determine if the - // first tab in the tab bar is selected. If it is, we make the `windowButtonsBackdrop` color the same - // as that of the active tab (i.e. the titlebar's background color), otherwise we make it the same - // color as the background of unselected tabs. - if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self), titlebarTabs { - windowButtonsBackdrop?.isHighlighted = index == 0 - } + effectViewIsHidden = true + } + + if titlebarTabs { + updateTabsForVeryDarkBackgrounds() + // This is called when we open, close, switch, and reorder tabs, at which point we determine if the + // first tab in the tab bar is selected. If it is, we make the `windowButtonsBackdrop` color the same + // as that of the active tab (i.e. the titlebar's background color), otherwise we make it the same + // color as the background of unselected tabs. + if let index = windowController?.window?.tabbedWindows?.firstIndex(of: self) { + windowButtonsBackdrop?.isHighlighted = index == 0 + } + } - // Color the new tab button's image to match the color of the tab title/keyboard shortcut labels, - // just as it does in the stock tab bar. updateNewTabButtonOpacity() - - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { - $0.className == "NSTitlebarContainerView" - }) else { return } - guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } - guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { - $0 as? NSImageView != nil - }) as? NSImageView else { return } - guard let newTabButtonImage = newTabButtonImageView.image else { return } - - let isLightTheme = backgroundColor.isLightColor - - if newTabButtonImageLayer == nil { - let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) - let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in - newTabButtonImage.draw(in: rect) - fillColor.setFill() - rect.fill(using: .sourceAtop) - return true - } - let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! - imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) - imageLayer.contentsGravity = .resizeAspect - imageLayer.contents = newImage - imageLayer.opacity = 0.5 - - newTabButtonImageLayer = imageLayer - } - - newTabButtonImageView.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() - newTabButtonImageView.layer?.addSublayer(newTabButtonImageLayer!) - newTabButtonImageView.image = nil - // When we nil out the original image, the image view's frame resizes and repositions - // slightly, so we need to reset it to make sure our new image doesn't shift quickly. - newTabButtonImageView.frame = newTabButton.bounds + updateNewTabButtonImage() + updateResetZoomTitlebarButtonVisibility() } // MARK: - @@ -205,6 +191,69 @@ class TerminalWindow: NSWindow { newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5 } + // Color the new tab button's image to match the color of the tab title/keyboard shortcut labels, + // just as it does in the stock tab bar. + private func updateNewTabButtonImage() { + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }) else { return } + guard let newTabButton: NSButton = titlebarContainer.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return } + guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { + $0 as? NSImageView != nil + }) as? NSImageView else { return } + guard let newTabButtonImage = newTabButtonImageView.image else { return } + + let isLightTheme = backgroundColor.isLightColor + + if newTabButtonImageLayer == nil { + let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) + let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in + newTabButtonImage.draw(in: rect) + fillColor.setFill() + rect.fill(using: .sourceAtop) + return true + } + let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)! + imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size) + imageLayer.contentsGravity = .resizeAspect + imageLayer.contents = newImage + imageLayer.opacity = 0.5 + + newTabButtonImageLayer = imageLayer + } + + newTabButtonImageView.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() + newTabButtonImageView.layer?.addSublayer(newTabButtonImageLayer!) + newTabButtonImageView.image = nil + // When we nil out the original image, the image view's frame resizes and repositions + // slightly, so we need to reset it to make sure our new image doesn't shift quickly. + newTabButtonImageView.frame = newTabButton.bounds + } + + var hasVeryDarkBackground: Bool { + backgroundColor.luminance < 0.05 + } + + lazy var backgroundColorWithOpacity: NSColor = backgroundColor.withAlphaComponent(titlebarOpacity) + + private func updateTabsForVeryDarkBackgrounds() { + guard hasVeryDarkBackground else { return } + + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { + $0.className == "NSTitlebarContainerView" + }) else { return } + + if let tabGroup = tabGroup, tabGroup.isTabBarVisible { + guard let activeTabBackgroundView = titlebarContainer.firstDescendant(withClassName: "NSTabButton")?.superview?.subviews.last?.firstDescendant(withID: "_backgroundView") + else { return } + + activeTabBackgroundView.layer?.backgroundColor = backgroundColorWithOpacity.cgColor + titlebarContainer.layer?.backgroundColor = backgroundColorWithOpacity.highlight(withLevel: 0.14)?.cgColor + } else { + titlebarContainer.layer?.backgroundColor = backgroundColorWithOpacity.cgColor + } + } + private func updateResetZoomTitlebarButtonVisibility() { guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return } @@ -407,9 +456,7 @@ class TerminalWindow: NSWindow { return } - let backdropColor = backgroundColor.withAlphaComponent(titlebarOpacity).usingColorSpace(colorSpace!)!.cgColor - - let view = WindowButtonsBackdropView(backgroundColor: backdropColor) + let view = WindowButtonsBackdropView(window: self) view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop") titlebarView.addSubview(view) @@ -491,8 +538,9 @@ fileprivate class WindowDragView: NSView { // A view that matches the color of selected and unselected tabs in the adjacent tab bar. fileprivate class WindowButtonsBackdropView: NSView { + private let terminalWindow: TerminalWindow + private let isLightTheme: Bool private let overlayLayer = VibrantLayer() - private let isLightTheme: Bool var isHighlighted: Bool = true { didSet { @@ -500,8 +548,14 @@ fileprivate class WindowButtonsBackdropView: NSView { overlayLayer.isHidden = isHighlighted layer?.backgroundColor = .clear } else { + let systemOverlayColor = NSColor(cgColor: CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.45))! + let titlebarBackgroundColor = terminalWindow.backgroundColorWithOpacity.blended(withFraction: 1, of: systemOverlayColor) + + let highlightedColor = terminalWindow.hasVeryDarkBackground ? terminalWindow.backgroundColor : .clear + let backgroundColor = terminalWindow.hasVeryDarkBackground ? titlebarBackgroundColor : systemOverlayColor + overlayLayer.isHidden = true - layer?.backgroundColor = isHighlighted ? .clear : CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.45) + layer?.backgroundColor = isHighlighted ? highlightedColor?.cgColor : backgroundColor?.cgColor } } } @@ -510,8 +564,9 @@ fileprivate class WindowButtonsBackdropView: NSView { fatalError("init(coder:) has not been implemented") } - init(backgroundColor: CGColor) { - self.isLightTheme = NSColor(cgColor: backgroundColor)!.isLightColor + init(window: TerminalWindow) { + self.terminalWindow = window + self.isLightTheme = window.backgroundColor.isLightColor super.init(frame: .zero) diff --git a/macos/Sources/Helpers/NSView+Extension.swift b/macos/Sources/Helpers/NSView+Extension.swift index 1fcaea380..b9234a49a 100644 --- a/macos/Sources/Helpers/NSView+Extension.swift +++ b/macos/Sources/Helpers/NSView+Extension.swift @@ -28,4 +28,17 @@ extension NSView { return result } + + /// Recursively finds and returns the first descendant view that has the given identifier. + func firstDescendant(withID id: String) -> NSView? { + for subview in subviews { + if subview.identifier == NSUserInterfaceItemIdentifier(id) { + return subview + } else if let found = subview.firstDescendant(withID: id) { + return found + } + } + + return nil + } } From a0a1c991b57165a1d3ff514c7ddddb30fc14f9c4 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Tue, 2 Apr 2024 22:31:23 +0200 Subject: [PATCH 4/8] Fix conflicting constraints issue --- macos/Sources/Features/Terminal/TerminalWindow.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 73a16b54b..09991b135 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -285,9 +285,9 @@ class TerminalWindow: NSWindow { toolbarStyle = .unifiedCompact if let resetZoomItem = terminalToolbar.items.first(where: { $0.itemIdentifier == .resetZoom }) { resetZoomItem.view = resetZoomToolbarButton - resetZoomItem.view?.translatesAutoresizingMaskIntoConstraints = false - resetZoomItem.view?.widthAnchor.constraint(equalToConstant: 22).isActive = true - resetZoomItem.view?.heightAnchor.constraint(equalToConstant: 20).isActive = true + resetZoomItem.view!.removeConstraints(resetZoomItem.view!.constraints) + resetZoomItem.view!.widthAnchor.constraint(equalToConstant: 22).isActive = true + resetZoomItem.view!.heightAnchor.constraint(equalToConstant: 20).isActive = true } updateResetZoomTitlebarButtonVisibility() } From e4066aaa85fa24afccd7f8764758f0c33d5f3697 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Wed, 3 Apr 2024 15:36:04 +0200 Subject: [PATCH 5/8] Put titlebar font code where it was intended --- .../Features/Terminal/TerminalWindow.swift | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index b89ab1ac2..6067a5aab 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -180,38 +180,6 @@ class TerminalWindow: NSWindow { updateResetZoomTitlebarButtonVisibility() } - // Used to set the titlebar font. - var titlebarFont: NSFont? { - didSet { - titlebarTextField?.font = titlebarFont - tab.attributedTitle = attributedTitle - - if let toolbar = toolbar as? TerminalToolbar { - toolbar.titleFont = titlebarFont - } - } - } - - // Find the NSTextField responsible for displaying the titlebar's title. - private var titlebarTextField: NSTextField? { - guard let titlebarContainer = contentView?.superview?.subviews - .first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } - guard let titlebarView = titlebarContainer.subviews - .first(where: { $0.className == "NSTitlebarView" }) else { return nil } - return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField - } - - // Return a styled representation of our title property. - private var attributedTitle: NSAttributedString? { - guard let titlebarFont else { return nil } - - let attributes: [NSAttributedString.Key: Any] = [ - .font: titlebarFont, - .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - return NSAttributedString(string: title, attributes: attributes) - } - // MARK: - private var newTabButtonImageLayer: VibrantLayer? = nil @@ -375,7 +343,39 @@ class TerminalWindow: NSWindow { } } } - + + // Used to set the titlebar font. + var titlebarFont: NSFont? { + didSet { + titlebarTextField?.font = titlebarFont + tab.attributedTitle = attributedTitle + + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleFont = titlebarFont + } + } + } + + // Find the NSTextField responsible for displaying the titlebar's title. + private var titlebarTextField: NSTextField? { + guard let titlebarContainer = contentView?.superview?.subviews + .first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } + guard let titlebarView = titlebarContainer.subviews + .first(where: { $0.className == "NSTitlebarView" }) else { return nil } + return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField + } + + // Return a styled representation of our title property. + private var attributedTitle: NSAttributedString? { + guard let titlebarFont else { return nil } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: titlebarFont, + .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + ] + return NSAttributedString(string: title, attributes: attributes) + } + private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil private var windowDragHandle: WindowDragView? = nil From 4ede25dd00541c798c7e46a5912f61f3d61aa9e0 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Fri, 5 Apr 2024 15:19:20 +0200 Subject: [PATCH 6/8] Update standard title/tab bar when config changes --- .../Terminal/TerminalController.swift | 21 +++-- .../Features/Terminal/TerminalWindow.swift | 76 +++++++++++-------- 2 files changed, 54 insertions(+), 43 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 3076a226c..7766a9c46 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -166,15 +166,9 @@ class TerminalController: NSWindowController, NSWindowDelegate, private func syncAppearance() { guard let window = self.window as? TerminalWindow else { return } - // We match the appearance depending on the lightness/darkness of the - // background color. We have to do this because our titlebars in tabs inherit - // our background color for the focused tab but use the macOS theme for the - // rest of the titlebar. - if (window.titlebarTabs) { - let color = OSColor(ghostty.config.backgroundColor) - let appearance = NSAppearance(named: color.isLightColor ? .aqua : .darkAqua) - window.appearance = appearance - } + let backgroundColor = OSColor(ghostty.config.backgroundColor) + let appearance = NSAppearance(named: backgroundColor.isLightColor ? .aqua : .darkAqua) + window.appearance = appearance // Set the font for the window and tab titles. if let titleFontName = ghostty.config.windowTitleFontFamily { @@ -182,6 +176,10 @@ class TerminalController: NSWindowController, NSWindowDelegate, } else { window.titlebarFont = nil } + + window.backgroundColor = backgroundColor + window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) + window.updateToolbar() } /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about @@ -254,10 +252,11 @@ class TerminalController: NSWindowController, NSWindowDelegate, window.center() // Set the background color of the window - window.backgroundColor = NSColor(ghostty.config.backgroundColor) + let backgroundColor = NSColor(ghostty.config.backgroundColor) + window.backgroundColor = backgroundColor // This makes sure our titlebar renders correctly when there is a transparent background - window.titlebarOpacity = ghostty.config.backgroundOpacity + window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) // Handle titlebar tabs config option. Something about what we do while setting up the // titlebar tabs interferes with the window restore process unless window.tabbingMode diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 6067a5aab..31f525b17 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -4,14 +4,14 @@ class TerminalWindow: NSWindow { @objc dynamic var surfaceIsZoomed: Bool = false @objc dynamic var keyEquivalent: String = "" - var titlebarOpacity: CGFloat = 1 { + lazy var titlebarColor: NSColor = backgroundColor { didSet { guard let titlebarContainer = contentView?.superview?.subviews.first(where: { $0.className == "NSTitlebarContainerView" }) else { return } titlebarContainer.wantsLayer = true - titlebarContainer.layer?.backgroundColor = backgroundColor.withAlphaComponent(titlebarOpacity).cgColor + titlebarContainer.layer?.backgroundColor = titlebarColor.cgColor } } @@ -182,6 +182,12 @@ class TerminalWindow: NSWindow { // MARK: - + func updateToolbar() { + newTabButtonImageLayer = nil + effectViewIsHidden = false + } + + private var newTabButtonImage: NSImage? = nil private var newTabButtonImageLayer: VibrantLayer? = nil // Since we are coloring the new tab button's image, it doesn't respond to the @@ -209,7 +215,12 @@ class TerminalWindow: NSWindow { guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { $0 as? NSImageView != nil }) as? NSImageView else { return } - guard let newTabButtonImage = newTabButtonImageView.image else { return } + + if newTabButtonImage == nil { + newTabButtonImage = newTabButtonImageView.image + } + + guard let newTabButtonImage else { return } let isLightTheme = backgroundColor.isLightColor @@ -242,8 +253,6 @@ class TerminalWindow: NSWindow { backgroundColor.luminance < 0.05 } - lazy var backgroundColorWithOpacity: NSColor = backgroundColor.withAlphaComponent(titlebarOpacity) - private func updateTabsForVeryDarkBackgrounds() { guard hasVeryDarkBackground else { return } @@ -255,13 +264,15 @@ class TerminalWindow: NSWindow { guard let activeTabBackgroundView = titlebarContainer.firstDescendant(withClassName: "NSTabButton")?.superview?.subviews.last?.firstDescendant(withID: "_backgroundView") else { return } - activeTabBackgroundView.layer?.backgroundColor = backgroundColorWithOpacity.cgColor - titlebarContainer.layer?.backgroundColor = backgroundColorWithOpacity.highlight(withLevel: 0.14)?.cgColor + activeTabBackgroundView.layer?.backgroundColor = titlebarColor.cgColor + titlebarContainer.layer?.backgroundColor = titlebarColor.highlight(withLevel: 0.14)?.cgColor } else { - titlebarContainer.layer?.backgroundColor = backgroundColorWithOpacity.cgColor + titlebarContainer.layer?.backgroundColor = titlebarColor.cgColor } } + // MARK: - Split zooming + private func updateResetZoomTitlebarButtonVisibility() { guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return } @@ -282,24 +293,6 @@ class TerminalWindow: NSWindow { } } - // We have to regenerate a toolbar when the titlebar tabs setting changes since our - // custom toolbar conditionally generates the items based on this setting. I tried to - // invalidate the toolbar items and force a refresh, but as far as I can tell that - // isn't possible. - private func generateToolbar() { - let terminalToolbar = TerminalToolbar(identifier: "Toolbar") - - toolbar = terminalToolbar - toolbarStyle = .unifiedCompact - if let resetZoomItem = terminalToolbar.items.first(where: { $0.itemIdentifier == .resetZoom }) { - resetZoomItem.view = resetZoomToolbarButton - resetZoomItem.view!.removeConstraints(resetZoomItem.view!.constraints) - resetZoomItem.view!.widthAnchor.constraint(equalToConstant: 22).isActive = true - resetZoomItem.view!.heightAnchor.constraint(equalToConstant: 20).isActive = true - } - updateResetZoomTitlebarButtonVisibility() - } - private func generateResetZoomButton() -> NSButton { let button = NSButton() button.target = nil @@ -356,6 +349,24 @@ class TerminalWindow: NSWindow { } } + // We have to regenerate a toolbar when the titlebar tabs setting changes since our + // custom toolbar conditionally generates the items based on this setting. I tried to + // invalidate the toolbar items and force a refresh, but as far as I can tell that + // isn't possible. + func generateToolbar() { + let terminalToolbar = TerminalToolbar(identifier: "Toolbar") + + toolbar = terminalToolbar + toolbarStyle = .unifiedCompact + if let resetZoomItem = terminalToolbar.items.first(where: { $0.itemIdentifier == .resetZoom }) { + resetZoomItem.view = resetZoomToolbarButton + resetZoomItem.view!.removeConstraints(resetZoomItem.view!.constraints) + resetZoomItem.view!.widthAnchor.constraint(equalToConstant: 22).isActive = true + resetZoomItem.view!.heightAnchor.constraint(equalToConstant: 20).isActive = true + } + updateResetZoomTitlebarButtonVisibility() + } + // Find the NSTextField responsible for displaying the titlebar's title. private var titlebarTextField: NSTextField? { guard let titlebarContainer = contentView?.superview?.subviews @@ -382,14 +393,15 @@ class TerminalWindow: NSWindow { // The tab bar controller ID from macOS static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController") - // Look through the titlebar's view hierarchy and hide any of the internal - // views used to create a separator between the title/toolbar and unselected - // tabs in the tab bar. override func updateConstraintsIfNeeded() { super.updateConstraintsIfNeeded() - // For titlebar tabs, we want to hide the separator view so that we get rid - // of an aesthetically unpleasing shadow. + hideTitleBarSeparators() + } + + // For titlebar tabs, we want to hide the separator view so that we get rid + // of an aesthetically unpleasing shadow. + private func hideTitleBarSeparators() { guard titlebarTabs else { return } guard let titlebarContainer = contentView?.superview?.subviews.first(where: { @@ -589,7 +601,7 @@ fileprivate class WindowButtonsBackdropView: NSView { layer?.backgroundColor = .clear } else { let systemOverlayColor = NSColor(cgColor: CGColor(genericGrayGamma2_2Gray: 0.0, alpha: 0.45))! - let titlebarBackgroundColor = terminalWindow.backgroundColorWithOpacity.blended(withFraction: 1, of: systemOverlayColor) + let titlebarBackgroundColor = terminalWindow.titlebarColor.blended(withFraction: 1, of: systemOverlayColor) let highlightedColor = terminalWindow.hasVeryDarkBackground ? terminalWindow.backgroundColor : .clear let backgroundColor = terminalWindow.hasVeryDarkBackground ? titlebarBackgroundColor : systemOverlayColor From b947ed0070c6705b78a64c5b31efe1c928a38784 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Fri, 5 Apr 2024 16:40:04 +0200 Subject: [PATCH 7/8] Update titlebar tabs when config changes --- .../Terminal/TerminalController.swift | 2 +- .../Features/Terminal/TerminalWindow.swift | 213 +++++++++--------- 2 files changed, 109 insertions(+), 106 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 7766a9c46..ce22d3c3b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -179,7 +179,7 @@ class TerminalController: NSWindowController, NSWindowDelegate, window.backgroundColor = backgroundColor window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity) - window.updateToolbar() + window.updateTabBar() } /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 31f525b17..5ba0899d6 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -1,7 +1,6 @@ import Cocoa class TerminalWindow: NSWindow { - @objc dynamic var surfaceIsZoomed: Bool = false @objc dynamic var keyEquivalent: String = "" lazy var titlebarColor: NSColor = backgroundColor { @@ -15,32 +14,6 @@ class TerminalWindow: NSWindow { } } - private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() - - private lazy var resetZoomTabButton: NSButton = { - let button = generateResetZoomButton() - button.action = #selector(selectTabAndZoom(_:)) - return button - }() - - private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } - - let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) - let view = NSView(frame: NSRect(origin: .zero, size: size)) - - let button = generateResetZoomButton() - button.frame.origin.x = size.width/2 - button.bounds.width/2 - button.frame.origin.y = size.height/2 - button.bounds.height/2 - view.addSubview(button) - - let titlebarAccessoryViewController = NSTitlebarAccessoryViewController() - titlebarAccessoryViewController.view = view - titlebarAccessoryViewController.layoutAttribute = .right - - return titlebarAccessoryViewController - }() - private lazy var keyEquivalentLabel: NSTextField = { let label = NSTextField(labelWithAttributedString: NSAttributedString()) label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) @@ -102,7 +75,13 @@ class TerminalWindow: NSWindow { tab.attributedTitle = attributedTitle } } - + + // We only need to set this once, but need to do it after the window has been created in order + // to determine if the theme is using a very dark background, in which case we don't want to + // remove the effect view if the default tab bar is being used since the effect created in + // `updateTabsForVeryDarkBackgrounds`. + private var effectViewIsHidden = false + override func becomeKey() { // This is required because the removeTitlebarAccessoryViewController hook does not // catch the creation of a new window by "tearing off" a tab from a tabbed window. @@ -137,12 +116,6 @@ class TerminalWindow: NSWindow { updateTabsForVeryDarkBackgrounds() } - // We only need to set this once, but need to do it after the window has been created in order - // to determine if the theme is using a very dark background, in which case we don't want to - // remove the effect view if the default tab bar is being used since the effect created in - // `updateTabsForVeryDarkBackgrounds`. - private var effectViewIsHidden = false - override func update() { super.update() @@ -179,17 +152,37 @@ class TerminalWindow: NSWindow { updateNewTabButtonImage() updateResetZoomTitlebarButtonVisibility() } - - // MARK: - - func updateToolbar() { - newTabButtonImageLayer = nil - effectViewIsHidden = false + override func updateConstraintsIfNeeded() { + super.updateConstraintsIfNeeded() + + if titlebarTabs { + hideTitleBarSeparators() + } + } + + // MARK: - Tab Bar Styling + + var hasVeryDarkBackground: Bool { + backgroundColor.luminance < 0.05 } private var newTabButtonImage: NSImage? = nil + private var newTabButtonImageLayer: VibrantLayer? = nil + func updateTabBar() { + newTabButtonImageLayer = nil + effectViewIsHidden = false + + if titlebarTabs { + guard let tabBarAccessoryViewController = titlebarAccessoryViewControllers.first(where: { $0.identifier == Self.TabBarController}) else { return } + + tabBarAccessoryViewController.layoutAttribute = .right + pushTabsToTitlebar(tabBarAccessoryViewController) + } + } + // Since we are coloring the new tab button's image, it doesn't respond to the // window's key status changes in terms of becoming less prominent visually, // so we need to do it manually. @@ -249,10 +242,6 @@ class TerminalWindow: NSWindow { newTabButtonImageView.frame = newTabButton.bounds } - var hasVeryDarkBackground: Bool { - backgroundColor.luminance < 0.05 - } - private func updateTabsForVeryDarkBackgrounds() { guard hasVeryDarkBackground else { return } @@ -271,7 +260,35 @@ class TerminalWindow: NSWindow { } } - // MARK: - Split zooming + // MARK: - Split Zoom Button + + @objc dynamic var surfaceIsZoomed: Bool = false + + private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() + + private lazy var resetZoomTabButton: NSButton = { + let button = generateResetZoomButton() + button.action = #selector(selectTabAndZoom(_:)) + return button + }() + + private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { + guard let titlebarContainer = contentView?.superview?.subviews.first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } + + let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) + let view = NSView(frame: NSRect(origin: .zero, size: size)) + + let button = generateResetZoomButton() + button.frame.origin.x = size.width/2 - button.bounds.width/2 + button.frame.origin.y = size.height/2 - button.bounds.height/2 + view.addSubview(button) + + let titlebarAccessoryViewController = NSTitlebarAccessoryViewController() + titlebarAccessoryViewController.view = view + titlebarAccessoryViewController.layoutAttribute = .right + + return titlebarAccessoryViewController + }() private func updateResetZoomTitlebarButtonVisibility() { guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return } @@ -325,8 +342,49 @@ class TerminalWindow: NSWindow { windowController.splitZoom(self) } + // MARK: - Titlebar Font + + // Used to set the titlebar font. + var titlebarFont: NSFont? { + didSet { + titlebarTextField?.font = titlebarFont + tab.attributedTitle = attributedTitle + + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleFont = titlebarFont + } + } + } + + // Find the NSTextField responsible for displaying the titlebar's title. + private var titlebarTextField: NSTextField? { + guard let titlebarContainer = contentView?.superview?.subviews + .first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } + guard let titlebarView = titlebarContainer.subviews + .first(where: { $0.className == "NSTitlebarView" }) else { return nil } + return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField + } + + // Return a styled representation of our title property. + private var attributedTitle: NSAttributedString? { + guard let titlebarFont else { return nil } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: titlebarFont, + .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + ] + return NSAttributedString(string: title, attributes: attributes) + } + // MARK: - Titlebar Tabs + + private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil + private var windowDragHandle: WindowDragView? = nil + + // The tab bar controller ID from macOS + static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController") + // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { didSet { @@ -337,18 +395,6 @@ class TerminalWindow: NSWindow { } } - // Used to set the titlebar font. - var titlebarFont: NSFont? { - didSet { - titlebarTextField?.font = titlebarFont - tab.attributedTitle = attributedTitle - - if let toolbar = toolbar as? TerminalToolbar { - toolbar.titleFont = titlebarFont - } - } - } - // We have to regenerate a toolbar when the titlebar tabs setting changes since our // custom toolbar conditionally generates the items based on this setting. I tried to // invalidate the toolbar items and force a refresh, but as far as I can tell that @@ -367,43 +413,9 @@ class TerminalWindow: NSWindow { updateResetZoomTitlebarButtonVisibility() } - // Find the NSTextField responsible for displaying the titlebar's title. - private var titlebarTextField: NSTextField? { - guard let titlebarContainer = contentView?.superview?.subviews - .first(where: { $0.className == "NSTitlebarContainerView" }) else { return nil } - guard let titlebarView = titlebarContainer.subviews - .first(where: { $0.className == "NSTitlebarView" }) else { return nil } - return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField - } - - // Return a styled representation of our title property. - private var attributedTitle: NSAttributedString? { - guard let titlebarFont else { return nil } - - let attributes: [NSAttributedString.Key: Any] = [ - .font: titlebarFont, - .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - return NSAttributedString(string: title, attributes: attributes) - } - - private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil - private var windowDragHandle: WindowDragView? = nil - - // The tab bar controller ID from macOS - static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController") - - override func updateConstraintsIfNeeded() { - super.updateConstraintsIfNeeded() - - hideTitleBarSeparators() - } - // For titlebar tabs, we want to hide the separator view so that we get rid // of an aesthetically unpleasing shadow. private func hideTitleBarSeparators() { - guard titlebarTabs else { return } - guard let titlebarContainer = contentView?.superview?.subviews.first(where: { $0.className == "NSTitlebarContainerView" }) else { return } @@ -490,24 +502,15 @@ class TerminalWindow: NSWindow { // 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) - } +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { +// self.markHierarchyForLayout(accessoryView) +// } } private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) { - // If we already made the view, just make sure it's unhidden and correctly placed as a subview. - if let view = windowButtonsBackdrop { - view.removeFromSuperview() - view.isHidden = false - titlebarView.addSubview(view) - view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true - view.rightAnchor.constraint(equalTo: toolbarView.leftAnchor, constant: 78).isActive = true - view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true - view.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true - return - } - + windowButtonsBackdrop?.removeFromSuperview() + windowButtonsBackdrop = nil + let view = WindowButtonsBackdropView(window: self) view.identifier = NSUserInterfaceItemIdentifier("_windowButtonsBackdrop") titlebarView.addSubview(view) From 620c0f9450f438a636244f9084276babe7a327a8 Mon Sep 17 00:00:00 2001 From: Pete Schaffner Date: Fri, 5 Apr 2024 21:12:11 +0200 Subject: [PATCH 8/8] Fix new tab icon color not updating sometimes --- .../Features/Terminal/TerminalWindow.swift | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 5ba0899d6..cd4e77f33 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -167,8 +167,6 @@ class TerminalWindow: NSWindow { backgroundColor.luminance < 0.05 } - private var newTabButtonImage: NSImage? = nil - private var newTabButtonImageLayer: VibrantLayer? = nil func updateTabBar() { @@ -208,16 +206,11 @@ class TerminalWindow: NSWindow { guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: { $0 as? NSImageView != nil }) as? NSImageView else { return } + guard let newTabButtonImage = newTabButtonImageView.image else { return } - if newTabButtonImage == nil { - newTabButtonImage = newTabButtonImageView.image - } - guard let newTabButtonImage else { return } - - let isLightTheme = backgroundColor.isLightColor - - if newTabButtonImageLayer == nil { + if newTabButtonImageLayer == nil { + let isLightTheme = backgroundColor.isLightColor let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85) let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in newTabButtonImage.draw(in: rect) @@ -234,12 +227,9 @@ class TerminalWindow: NSWindow { newTabButtonImageLayer = imageLayer } - newTabButtonImageView.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() - newTabButtonImageView.layer?.addSublayer(newTabButtonImageLayer!) - newTabButtonImageView.image = nil - // When we nil out the original image, the image view's frame resizes and repositions - // slightly, so we need to reset it to make sure our new image doesn't shift quickly. - newTabButtonImageView.frame = newTabButton.bounds + newTabButtonImageView.isHidden = true + newTabButton.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer() + newTabButton.layer?.addSublayer(newTabButtonImageLayer!) } private func updateTabsForVeryDarkBackgrounds() {