Use template rendering so provider icons automatically follow the menu bar appearance. Co-authored-by: Cursor <cursoragent@cursor.com>
157 lines
5.1 KiB
Swift
157 lines
5.1 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
enum ProviderIconAsset {
|
|
static func name(for provider: ProviderID) -> String {
|
|
switch provider {
|
|
case .cursor: "ProviderCursor"
|
|
case .claude: "ProviderClaude"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum MenuBarIconRenderer {
|
|
/// MenuBarExtra ignores SwiftUI layout and NSImage.size — rasterize to exact points.
|
|
static func sizedImage(for provider: ProviderID, height: CGFloat = MenuBarMetrics.iconHeight) -> NSImage {
|
|
let source = loadProviderImage(for: provider)
|
|
guard let source else {
|
|
return NSImage(size: NSSize(width: height, height: height))
|
|
}
|
|
|
|
var sourceSize = source.size
|
|
if sourceSize.width <= 0 || sourceSize.height <= 0 {
|
|
sourceSize = NSSize(width: 24, height: 24)
|
|
}
|
|
|
|
let aspect = sourceSize.width / sourceSize.height
|
|
let pointSize = NSSize(width: height * aspect, height: height)
|
|
let scale = NSScreen.main?.backingScaleFactor ?? 2
|
|
let pixelWidth = max(Int((pointSize.width * scale).rounded(.up)), 1)
|
|
let pixelHeight = max(Int((pointSize.height * scale).rounded(.up)), 1)
|
|
|
|
guard let rep = NSBitmapImageRep(
|
|
bitmapDataPlanes: nil,
|
|
pixelsWide: pixelWidth,
|
|
pixelsHigh: pixelHeight,
|
|
bitsPerSample: 8,
|
|
samplesPerPixel: 4,
|
|
hasAlpha: true,
|
|
isPlanar: false,
|
|
colorSpaceName: .deviceRGB,
|
|
bytesPerRow: 0,
|
|
bitsPerPixel: 0
|
|
) else {
|
|
return NSImage(size: pointSize)
|
|
}
|
|
rep.size = pointSize
|
|
|
|
NSGraphicsContext.saveGraphicsState()
|
|
if let context = NSGraphicsContext(bitmapImageRep: rep) {
|
|
NSGraphicsContext.current = context
|
|
context.imageInterpolation = .high
|
|
|
|
let dest = NSRect(origin: .zero, size: pointSize)
|
|
let src = NSRect(origin: .zero, size: sourceSize)
|
|
|
|
source.draw(
|
|
in: dest,
|
|
from: src,
|
|
operation: .sourceOver,
|
|
fraction: 1.0,
|
|
respectFlipped: false,
|
|
hints: [.interpolation: NSImageInterpolation.high]
|
|
)
|
|
}
|
|
NSGraphicsContext.restoreGraphicsState()
|
|
|
|
let image = NSImage(size: pointSize)
|
|
image.addRepresentation(rep)
|
|
image.isTemplate = true
|
|
return image
|
|
}
|
|
|
|
private static func loadProviderImage(for provider: ProviderID) -> NSImage? {
|
|
var image: NSImage?
|
|
NSApp.effectiveAppearance.performAsCurrentDrawingAppearance {
|
|
image = NSImage(named: ProviderIconAsset.name(for: provider))
|
|
}
|
|
return image
|
|
}
|
|
}
|
|
|
|
/// Settings and other in-app UI — keep full size.
|
|
struct ProviderBrandIcon: View {
|
|
let provider: ProviderID
|
|
var size: CGFloat = 18
|
|
|
|
var body: some View {
|
|
Image(ProviderIconAsset.name(for: provider))
|
|
.resizable()
|
|
.interpolation(.high)
|
|
.antialiased(true)
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: size, height: size)
|
|
}
|
|
}
|
|
|
|
enum MenuBarMetrics {
|
|
static let iconHeight: CGFloat = 18
|
|
static let textSize: CGFloat = 11
|
|
static let providerSpacing: CGFloat = 8
|
|
static let itemSpacing: CGFloat = 4
|
|
static let labelHeight: CGFloat = 20
|
|
}
|
|
|
|
struct ProviderMenuBarIconView: View {
|
|
let providerID: ProviderID
|
|
let state: ProviderDisplayState?
|
|
let settings: AppSettings
|
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
private var selectedMetrics: [UsageMetric] {
|
|
settings.settings(for: providerID).menuBarMetrics
|
|
}
|
|
|
|
private var badgeText: String? {
|
|
guard let snapshot = state?.snapshot else { return nil }
|
|
return snapshot.menuBarDisplay(for: selectedMetrics)
|
|
}
|
|
|
|
/// Always show icon + text — this app has no Dock icon.
|
|
private var statusText: String {
|
|
if state?.isLoading == true { return "…" }
|
|
if let badgeText { return badgeText }
|
|
if state?.errorMessage != nil { return "!" }
|
|
return "—"
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: MenuBarMetrics.itemSpacing) {
|
|
Image(nsImage: MenuBarIconRenderer.sizedImage(for: providerID))
|
|
.renderingMode(.template)
|
|
.foregroundStyle(.primary)
|
|
.frame(width: MenuBarMetrics.iconHeight, height: MenuBarMetrics.iconHeight)
|
|
.opacity(state?.errorMessage != nil ? 0.45 : 1.0)
|
|
|
|
Text(statusText)
|
|
.font(.system(size: MenuBarMetrics.textSize, weight: .medium))
|
|
.foregroundStyle(state?.errorMessage != nil ? .red : .primary)
|
|
.lineLimit(1)
|
|
}
|
|
.fixedSize(horizontal: true, vertical: false)
|
|
.frame(height: MenuBarMetrics.labelHeight)
|
|
.id(colorScheme)
|
|
.help(helpText)
|
|
}
|
|
|
|
private var helpText: String {
|
|
if let error = state?.errorMessage {
|
|
return "\(providerID.displayName): \(error)"
|
|
}
|
|
if statusText != "—", statusText != "…" {
|
|
return "\(providerID.displayName): \(statusText)"
|
|
}
|
|
return providerID.displayName
|
|
}
|
|
}
|