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

149 lines
5.6 KiB
Swift

import AppKit
import CodexBarCore
extension StatusItemController {
private static let measuredStandardMenuWidthCacheLimit = 96
func menuCardWidth(
for providers: [UsageProvider],
selectedProvider: UsageProvider?,
descriptor: MenuDescriptor) -> CGFloat
{
let sectionSets: [[MenuDescriptor.Section]] = if self.shouldMergeIcons, providers.count > 1 {
providers.map { provider in
if provider == selectedProvider {
return descriptor.sections
}
return self.makeMenuDescriptor(
provider: provider,
includeContextualActions: true).sections
}
} else {
[descriptor.sections]
}
return self.measuredMenuCardWidth(for: sectionSets)
}
func measuredMenuCardWidth(for sectionSets: [[MenuDescriptor.Section]]) -> CGFloat {
let baselineWidth = Self.menuCardBaseWidth
return sectionSets.reduce(baselineWidth) { width, sections in
max(width, self.measuredStandardMenuWidth(for: sections, baseWidth: baselineWidth))
}
}
func makeMenuDescriptor(
provider: UsageProvider?,
includeContextualActions: Bool) -> MenuDescriptor
{
MenuDescriptor.build(
provider: provider,
store: self.store,
settings: self.settings,
account: self.account,
managedCodexAccountCoordinator: self.managedCodexAccountCoordinator,
codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator,
updateReady: self.updater.updateStatus.isUpdateReady,
includeContextualActions: includeContextualActions,
agentSessionsEnabled: self.settings.agentSessionsEnabled,
localAgentSessions: self.agentSessions.localSessions,
remoteAgentHosts: self.agentSessions.remoteHosts)
}
func measuredStandardMenuWidth(for sections: [MenuDescriptor.Section], baseWidth: CGFloat) -> CGFloat {
let cacheKey = self.measuredStandardMenuWidthCacheKey(for: sections, baseWidth: baseWidth)
if let cached = self.measuredStandardMenuWidthCache[cacheKey] {
return cached
}
let measuringMenu = NSMenu()
measuringMenu.autoenablesItems = false
self.addActionableSections(sections, to: measuringMenu, width: baseWidth)
let measured = ceil(measuringMenu.size.width)
if self.measuredStandardMenuWidthCache.count >= Self.measuredStandardMenuWidthCacheLimit {
self.measuredStandardMenuWidthCache.removeAll(keepingCapacity: true)
}
self.measuredStandardMenuWidthCache[cacheKey] = measured
return measured
}
private func measuredStandardMenuWidthCacheKey(
for sections: [MenuDescriptor.Section],
baseWidth: CGFloat) -> String
{
var parts = [
"base=\(Int((baseWidth * 100).rounded()))",
"font=\(Self.menuCardHeightTextScaleToken())",
self.menuLocalizationSignature(),
]
for section in sections {
parts.append("[")
for entry in section.entries {
parts.append(self.measuredStandardMenuWidthCacheToken(for: entry))
}
parts.append("]")
}
return parts.joined(separator: "\u{1f}")
}
private func measuredStandardMenuWidthCacheToken(for entry: MenuDescriptor.Entry) -> String {
switch entry {
case let .text(text, style):
"text:\(style):\(text)"
case let .action(title, action):
"action:\(title):\(self.measuredStandardMenuWidthCacheToken(for: action))"
case let .unavailable(title, tooltip):
"unavailable:\(title):\(tooltip ?? "")"
case let .submenu(title, systemImageName, submenuItems):
"submenu:\(title):\(systemImageName ?? ""):" + submenuItems.map { item in
[
item.title,
item.isEnabled ? "1" : "0",
item.isChecked ? "1" : "0",
item.action.map(self.measuredStandardMenuWidthCacheToken(for:)) ?? "",
].joined(separator: ":")
}.joined(separator: ",")
case .divider:
"divider"
}
}
private func measuredStandardMenuWidthCacheToken(for action: MenuDescriptor.MenuAction) -> String {
switch action {
case .installUpdate:
"installUpdate"
case .refresh:
"refresh"
case .refreshAugmentSession:
"refreshAugmentSession"
case .dashboard:
"dashboard"
case .statusPage:
"statusPage"
case .changelog:
"changelog"
case .addCodexAccount:
"addCodexAccount:\(self.codexAddAccountSubtitle() ?? "")"
case let .requestCodexSystemPromotion(id):
"requestCodexSystemPromotion:\(id)"
case let .addProviderAccount(provider):
"addProviderAccount:\(provider.rawValue)"
case let .switchAccount(provider):
"switchAccount:\(provider.rawValue):\(self.switchAccountSubtitle(for: provider) ?? "")"
case let .openTerminal(command):
"openTerminal:\(command)"
case let .loginToProvider(url):
"loginToProvider:\(url)"
case .settings:
"settings"
case .about:
"about"
case .quit:
"quit"
case let .copyError(message):
"copyError:\(message)"
case let .focusAgentSession(session, remoteHost):
"focusAgentSession:\(remoteHost ?? "local"):\(session.id)"
}
}
}