From f79ae518d4edc3b5b33236cf427eef9b294e1f9c Mon Sep 17 00:00:00 2001 From: Blake Williams Date: Sat, 28 Dec 2024 12:52:37 -0500 Subject: [PATCH] Use foreground color in macOS tab titles Currently tab titles will use the default macOS tab foreground color, but the color schemes background color. This can result in tabs with unreadable text. This change updates the macOS UI tabs to use the foreground color defined in the config, or the overridden value when set dynamically. This change: - Tracks `foregroundColor` in the SurfaceView which is pre-populated with the config foreground color along with support for color change update events. - Adds a new `titleForegroundColor` property to use in `TerminalWindow` titles. - Updates each tab window foreground color when `syncAppearance` is called. - Calls `syncAppearance` when a window becomes the key window. --- .../Terminal/TerminalController.swift | 35 ++++++++++++---- .../Features/Terminal/TerminalToolbar.swift | 10 +++++ .../Features/Terminal/TerminalWindow.swift | 41 +++++++++++++------ macos/Sources/Ghostty/Ghostty.Config.swift | 20 +++++++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 12 ++++++ 5 files changed, 98 insertions(+), 20 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ef4054c2e..4da607b93 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -253,20 +253,25 @@ class TerminalController: BaseTerminalController { // If it does, we match the focused surface. If it doesn't, we use the app // configuration. let backgroundColor: OSColor + let foregroundColor: OSColor if let surfaceTree { if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) { // Similar to above, an alpha component of "0" causes compositor issues, so // we use 0.001. See: https://github.com/ghostty-org/ghostty/pull/4308 backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.001) + foregroundColor = OSColor(focusedSurface.foregroundColor ?? surfaceConfig.foregroundColor) } else { // We don't have a focused surface or our surface doesn't border the // top. We choose to match the color of the top-left most surface. backgroundColor = OSColor(surfaceTree.topLeft().backgroundColor ?? derivedConfig.backgroundColor) + foregroundColor = OSColor(surfaceTree.topLeft().foregroundColor ?? derivedConfig.foregroundColor) } } else { backgroundColor = OSColor(self.derivedConfig.backgroundColor) + foregroundColor = OSColor(self.derivedConfig.foregroundColor) } window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity) + window.titleForegroundColor = foregroundColor if (window.isOpaque) { // Bg color is only synced if we have no transparency. This is because @@ -278,6 +283,11 @@ class TerminalController: BaseTerminalController { // so we only call this if we are opaque. window.updateTabBar() } + + guard let windows = self.window?.tabbedWindows as? [TerminalWindow] else { return } + for (w) in windows { + w.titleForegroundColor = window.titleForegroundColor + } } private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { @@ -315,28 +325,28 @@ class TerminalController: BaseTerminalController { window.styleMask = [ // We need `titled` in the mask to get the normal window frame .titled, - + // Full size content view so we can extend // content in to the hidden titlebar's area .fullSizeContentView, - + .resizable, .closable, .miniaturizable, ] - + // Hide the title window.titleVisibility = .hidden window.titlebarAppearsTransparent = true - + // Hide the traffic lights (window control buttons) window.standardWindowButton(.closeButton)?.isHidden = true window.standardWindowButton(.miniaturizeButton)?.isHidden = true window.standardWindowButton(.zoomButton)?.isHidden = true - + // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. window.tabbingMode = .disallowed - + // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are // some operations that appear to bring back the titlebar visibility so this ensures // it is gone forever. @@ -345,7 +355,7 @@ class TerminalController: BaseTerminalController { titleBarContainer.isHidden = true } } - + override func windowDidLoad() { super.windowDidLoad() guard let window = window as? TerminalWindow else { return } @@ -429,6 +439,7 @@ class TerminalController: BaseTerminalController { // This makes sure our titlebar renders correctly when there is a transparent background window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) + window.titleForegroundColor = NSColor(config.foregroundColor) } // Initialize our content view to the SwiftUI root @@ -487,6 +498,10 @@ class TerminalController: BaseTerminalController { super.windowDidBecomeKey(notification) self.relabelTabs() self.fixTabBar() + + if let focusedSurface { + self.syncAppearance(focusedSurface.derivedConfig) + } } override func windowDidMove(_ notification: Notification) { @@ -651,6 +666,9 @@ class TerminalController: BaseTerminalController { focusedSurface.$backgroundColor .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } .store(in: &surfaceAppearanceCancellables) + focusedSurface.$foregroundColor + .sink { [weak self, weak focusedSurface] _ in self?.syncAppearanceOnPropertyChange(focusedSurface) } + .store(in: &surfaceAppearanceCancellables) } private func syncAppearanceOnPropertyChange(_ surface: Ghostty.SurfaceView?) { @@ -780,15 +798,18 @@ class TerminalController: BaseTerminalController { private struct DerivedConfig { let backgroundColor: Color + let foregroundColor: Color let macosTitlebarStyle: String init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) + self.foregroundColor = Color(NSColor.labelColor) self.macosTitlebarStyle = "system" } init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor + self.foregroundColor = config.foregroundColor self.macosTitlebarStyle = config.macosTitlebarStyle } } diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index 87cb6ce4f..ebacc77ec 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -25,6 +25,16 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { } } + var titleColor: NSColor? { + get { + titleTextField.textColor + } + + set { + titleTextField.textColor = newValue + } + } + override init(identifier: NSToolbar.Identifier) { super.init(identifier: identifier) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 0eb8daeeb..652deaf87 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -8,6 +8,17 @@ class TerminalWindow: NSWindow { guard let titlebarContainer else { return } titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = titlebarColor.cgColor + tab.attributedTitle = attributedTitle + } + } + + lazy var titleForegroundColor: NSColor = NSColor.labelColor { + didSet { + tab.attributedTitle = attributedTitle + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleColor = titleForegroundColor + } + self.updateKeyEquivalentLabel() } } @@ -28,13 +39,7 @@ class TerminalWindow: NSWindow { }, 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 + self?.updateKeyEquivalentLabel() }, ] @@ -317,6 +322,17 @@ class TerminalWindow: NSWindow { } } + private func updateKeyEquivalentLabel() { + let attributes: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 11), + .foregroundColor: self.titleForegroundColor.withAlphaComponent(isKeyWindow ? 1 : 0.4), + ] + let attributedString = NSAttributedString(string: " \(keyEquivalent) ", attributes: attributes) + + keyEquivalentLabel.attributedStringValue = attributedString + } + + // MARK: - Split Zoom Button @objc dynamic var surfaceIsZoomed: Bool = false @@ -403,13 +419,13 @@ class TerminalWindow: NSWindow { // Used to set the titlebar font. var titlebarFont: NSFont? { didSet { - let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) + let font = titlebarFont ?? NSFont.titleBarFont(ofSize: 11) titlebarTextField?.font = font tab.attributedTitle = attributedTitle if let toolbar = toolbar as? TerminalToolbar { - toolbar.titleFont = font + toolbar.titleFont = font.withSize(NSFont.systemFontSize) } } } @@ -423,12 +439,11 @@ class TerminalWindow: NSWindow { // 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, + .foregroundColor: titleForegroundColor, + .font: titlebarFont?.withSize(11) ?? NSFont.systemFont(ofSize: 11), ] + return NSAttributedString(string: title, attributes: attributes) } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 1b3263fc3..ce3bb0d23 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -350,6 +350,26 @@ extension Ghostty { return v; } + var foregroundColor: Color { + var color: ghostty_config_color_s = .init(); + let bg_key = "foreground" + if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.count))) { +#if os(macOS) + return Color(NSColor.labelColor) +#elseif os(iOS) + return Color(UIColor.label) +#else +#error("unsupported") +#endif + } + + return .init( + red: Double(color.r) / 255, + green: Double(color.g) / 255, + blue: Double(color.b) / 255 + ) + } + var unfocusedSplitOpacity: Double { guard let config = self.config else { return 1 } var opacity: Double = 0.85 diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index c933eb9bf..f8c3d542d 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -55,6 +55,10 @@ extension Ghostty { /// dynamically updated. Otherwise, the background color is the default background color. @Published private(set) var backgroundColor: Color? = nil + /// The foreground color within the color palette of the surface. This is only set if it is + /// dynamically updated. Otherwise, the background color is the default background color. + @Published private(set) var foregroundColor: Color? = nil + // An initial size to request for a window. This will only affect // then the view is moved to a new window. var initialSize: NSSize? = nil @@ -443,6 +447,11 @@ extension Ghostty { DispatchQueue.main.async { [weak self] in self?.backgroundColor = change.color } + self.backgroundColor = change.color + case .foreground: + DispatchQueue.main.async { [weak self] in + self?.foregroundColor = change.color + } default: // We don't do anything for the other colors yet. @@ -1209,6 +1218,7 @@ extension Ghostty { struct DerivedConfig { let backgroundColor: Color let backgroundOpacity: Double + let foregroundColor: Color let macosWindowShadow: Bool let windowTitleFontFamily: String? let windowAppearance: NSAppearance? @@ -1216,6 +1226,7 @@ extension Ghostty { init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundOpacity = 1 + self.foregroundColor = Color(NSColor.labelColor) self.macosWindowShadow = true self.windowTitleFontFamily = nil self.windowAppearance = nil @@ -1224,6 +1235,7 @@ extension Ghostty { init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor self.backgroundOpacity = config.backgroundOpacity + self.foregroundColor = config.foregroundColor self.macosWindowShadow = config.macosWindowShadow self.windowTitleFontFamily = config.windowTitleFontFamily self.windowAppearance = .init(ghosttyConfig: config)