138 lines
5.1 KiB
Swift
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
|
|
}
|
|
}
|