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.
This commit is contained in:
Blake Williams
2024-12-28 12:52:37 -05:00
parent 19ffb0b51f
commit f79ae518d4
5 changed files with 98 additions and 20 deletions

View File

@ -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
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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

View File

@ -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)