Files
steipete--codexbar/Sources/CodexBar/StatusItemController+CostMenuCard.swift
T
2026-07-13 12:22:33 +08:00

138 lines
5.1 KiB
Swift

import AppKit
import SwiftUI
private struct CostMenuCardRowView: View {
let title: String
let detailLines: [String]
let width: CGFloat
@Environment(\.menuItemHighlighted) private var isHighlighted
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(self.title)
.font(.system(size: NSFont.menuFont(ofSize: 0).pointSize))
.lineLimit(1)
ForEach(self.detailLines.indices, id: \.self) { index in
Text(self.detailLines[index])
.font(.system(size: NSFont.smallSystemFontSize))
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
.truncationMode(.tail)
}
}
.padding(.leading, 20)
.padding(.trailing, 28)
.padding(.vertical, 6)
.frame(width: self.width, alignment: .leading)
}
}
extension StatusItemController {
static var costMenuTitle: String {
L("Cost")
}
func makeCostMenuCardItem(
model: UsageMenuCardView.Model,
submenu: NSMenu?,
width: CGFloat) -> NSMenuItem
{
let tooltipLines = Self.costMenuTooltipLines(tokenUsage: model.tokenUsage)
let visibleDetailLines = Self.costMenuVisibleDetailLines(
tokenUsage: model.tokenUsage,
hasSubmenu: submenu != nil)
guard visibleDetailLines.isEmpty == false, self.menuCardRenderingEnabledForController else {
return Self.makeNativeCostMenuCardItem(
visibleDetailLines: visibleDetailLines,
tooltipLines: tooltipLines,
submenu: submenu)
}
let item = self.makeMenuCardItem(
CostMenuCardRowView(
title: Self.costMenuTitle,
detailLines: visibleDetailLines,
width: width),
id: "menuCardCost",
width: width,
heightCacheScope: model.provider.rawValue,
heightCacheFingerprint: "costMenuRow:\(visibleDetailLines.count)",
submenu: submenu,
submenuIndicatorAlignment: .trailing,
submenuIndicatorTopPadding: 0)
item.title = Self.costMenuTitle
item.toolTip = tooltipLines.joined(separator: "\n")
return item
}
private static func makeNativeCostMenuCardItem(
visibleDetailLines: [String],
tooltipLines: [String],
submenu: NSMenu?) -> NSMenuItem
{
let item = NSMenuItem(title: Self.costMenuTitle, action: nil, keyEquivalent: "")
item.isEnabled = true
item.representedObject = "menuCardCost"
item.submenu = submenu
// Submenu cost rows already show these details; keep tooltips only for inline rows
// where they reveal truncated text and avoid flashes during in-place menu refreshes.
if submenu == nil {
item.toolTip = tooltipLines.joined(separator: "\n")
}
if #available(macOS 14.4, *) {
item.subtitle = visibleDetailLines.joined(separator: "\n")
} else if !visibleDetailLines.isEmpty {
item.attributedTitle = Self.costMenuFallbackAttributedTitle(visibleDetailLines: visibleDetailLines)
}
return item
}
static func costMenuTooltipLines(tokenUsage: UsageMenuCardView.Model.TokenUsageSection?) -> [String] {
let lines = [
tokenUsage?.sessionLine,
tokenUsage?.monthLine,
]
.compactMap(\.self)
+ (tokenUsage?.comparisonLines ?? [])
+ [tokenUsage?.hintLine, tokenUsage?.errorLine].compactMap(\.self)
return lines.filter { !$0.isEmpty }
}
static func costMenuVisibleDetailLines(
tokenUsage: UsageMenuCardView.Model.TokenUsageSection?,
hasSubmenu: Bool) -> [String]
{
guard !hasSubmenu else { return [] }
let primaryLines = ([
tokenUsage?.sessionLine,
tokenUsage?.monthLine,
]
.compactMap(\.self)
+ (tokenUsage?.comparisonLines ?? [])
+ [tokenUsage?.errorLine].compactMap(\.self))
.filter { !$0.isEmpty }
guard primaryLines.isEmpty else { return primaryLines }
return [tokenUsage?.hintLine]
.compactMap(\.self)
.filter { !$0.isEmpty }
}
static func costMenuFallbackAttributedTitle(visibleDetailLines: [String]) -> NSAttributedString {
let detailText = visibleDetailLines.joined(separator: " | ")
let title = detailText.isEmpty ? self.costMenuTitle : "\(self.costMenuTitle) \(detailText)"
let attributedTitle = NSMutableAttributedString(
string: title,
attributes: [.font: NSFont.menuFont(ofSize: NSFont.systemFontSize)])
guard !detailText.isEmpty else { return attributedTitle }
let detailRange = (title as NSString).range(of: detailText)
attributedTitle.addAttributes(
[
.font: NSFont.menuFont(ofSize: NSFont.smallSystemFontSize),
.foregroundColor: NSColor.secondaryLabelColor,
],
range: detailRange)
return attributedTitle
}
}