1520 lines · 60.4 KB · Swift
RawDownload
import AppKitimport CodexBarCoreimport SwiftUI enum UsageMenuCardLayout {    static let horizontalPadding: CGFloat = 20    static let headerOnlyVerticalPadding: CGFloat = 7    static let sectionTopPadding: CGFloat = 6    static let sectionBottomPadding: CGFloat = 6    static let headerLineSpacing: CGFloat = 4    static let headerColumnSpacing: CGFloat = 12} /// SwiftUI card used inside the NSMenu to mirror Apple's rich menu panels.struct UsageMenuCardView: View {    struct Model {        enum PercentStyle: String {            case left            case used             var labelSuffix: String {                switch self {                case .left: L("usage_percent_suffix_left")                case .used: L("usage_percent_suffix_used")                }            }             var accessibilityLabel: String {                switch self {                case .left: L("Usage remaining")                case .used: L("Usage used")                }            }        }         struct Metric: Identifiable {            let id: String            let title: String            let percent: Double            let percentStyle: PercentStyle            let statusText: String?            let resetText: String?            let detailText: String?            let detailLeftText: String?            let detailRightText: String?            let pacePercent: Double?            let paceOnTop: Bool            let warningMarkerPercents: [Double]            let cardStyle: Bool             init(                id: String,                title: String,                percent: Double,                percentStyle: PercentStyle,                statusText: String? = nil,                resetText: String?,                detailText: String?,                detailLeftText: String?,                detailRightText: String?,                pacePercent: Double?,                paceOnTop: Bool,                warningMarkerPercents: [Double] = [],                cardStyle: Bool = false)            {                self.id = id                self.title = title                self.percent = percent                self.percentStyle = percentStyle                self.statusText = statusText                self.resetText = resetText                self.detailText = detailText                self.detailLeftText = detailLeftText                self.detailRightText = detailRightText                self.pacePercent = pacePercent                self.paceOnTop = paceOnTop                self.warningMarkerPercents = warningMarkerPercents                self.cardStyle = cardStyle            }             var percentLabel: String {                String(format: "%.0f%% %@", self.percent, self.percentStyle.labelSuffix)            }        }         enum SubtitleStyle {            case info            case loading            case error        }         struct TokenUsageSection {            let sessionLine: String            let monthLine: String            let hintLine: String?            let errorLine: String?            let errorCopyText: String?        }         struct ProviderCostSection {            let title: String            let percentUsed: Double?            let spendLine: String            let percentLine: String?        }         let provider: UsageProvider        let providerName: String        let email: String        let subtitleText: String        let subtitleStyle: SubtitleStyle        let planText: String?        let metrics: [Metric]        let usageNotes: [String]        let openAIAPIUsage: OpenAIAPIUsageSnapshot?        let inlineUsageDashboard: InlineUsageDashboardModel?        let creditsText: String?        let creditsRemaining: Double?        let creditsHintText: String?        let creditsHintCopyText: String?        let providerCost: ProviderCostSection?        let tokenUsage: TokenUsageSection?        let placeholder: String?        let progressColor: Color    }     let model: Model    let width: CGFloat    @Environment(\.menuItemHighlighted) private var isHighlighted     static func popupMetricTitle(provider: UsageProvider, metric: Model.Metric) -> String {        if provider == .openrouter, metric.id == "primary" {            return L("API key limit")        }        return metric.title    }     var body: some View {        VStack(alignment: .leading, spacing: 6) {            UsageMenuCardHeaderView(model: self.model)             if self.hasDetails {                Divider()            }             if !self.model.usesStackedDetailLayout {                if let dashboard = self.model.inlineUsageDashboard {                    InlineUsageDashboardContent(model: dashboard)                } else if !self.model.usageNotes.isEmpty {                    UsageNotesContent(notes: self.model.usageNotes)                } else if let placeholder = self.model.placeholder {                    Text(placeholder)                        .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                        .font(.subheadline)                }            } else {                let hasUsage = self.model.hasUsageContent                let hasCredits = self.model.creditsText != nil                let hasProviderCost = self.model.providerCost != nil                let hasCost = self.model.tokenUsage != nil || hasProviderCost                 VStack(alignment: .leading, spacing: 12) {                    if hasUsage {                        VStack(alignment: .leading, spacing: 12) {                            ForEach(self.model.metrics, id: \.id) { metric in                                MetricRow(                                    metric: metric,                                    title: Self.popupMetricTitle(provider: self.model.provider, metric: metric),                                    progressColor: self.model.progressColor)                            }                            if let dashboard = self.model.inlineUsageDashboard {                                InlineUsageDashboardContent(model: dashboard)                            } else if !self.model.usageNotes.isEmpty {                                UsageNotesContent(notes: self.model.usageNotes)                            } else if let placeholder = self.model.placeholder {                                Text(placeholder)                                    .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                                    .font(.subheadline)                            }                        }                    }                    if hasUsage, hasCredits || hasCost {                        Divider()                    }                    if let credits = self.model.creditsText {                        CreditsBarContent(                            creditsText: credits,                            creditsRemaining: self.model.creditsRemaining,                            hintText: self.model.creditsHintText,                            hintCopyText: self.model.creditsHintCopyText,                            progressColor: self.model.progressColor)                    }                    if hasCredits, hasCost {                        Divider()                    }                    if let providerCost = self.model.providerCost {                        ProviderCostContent(                            section: providerCost,                            progressColor: self.model.progressColor)                    }                    if hasProviderCost, self.model.tokenUsage != nil {                        Divider()                    }                    if let tokenUsage = self.model.tokenUsage {                        VStack(alignment: .leading, spacing: 6) {                            Text(L("cost_header_estimated"))                                .font(.body)                                .fontWeight(.medium)                            Text(tokenUsage.sessionLine)                                .font(.footnote)                            Text(tokenUsage.monthLine)                                .font(.footnote)                            if let hint = tokenUsage.hintLine, !hint.isEmpty {                                Text(hint)                                    .font(.footnote)                                    .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                                    .lineLimit(4)                                    .fixedSize(horizontal: false, vertical: true)                            }                            if let error = tokenUsage.errorLine, !error.isEmpty {                                Text(error)                                    .font(.footnote)                                    .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted))                                    .lineLimit(4)                                    .fixedSize(horizontal: false, vertical: true)                                    .overlay {                                        ClickToCopyOverlay(copyText: tokenUsage.errorCopyText ?? error)                                    }                            }                        }                    }                }                .padding(.bottom, self.model.creditsText == nil ? 6 : 0)            }        }        .padding(.horizontal, UsageMenuCardLayout.horizontalPadding)        .padding(            .top,            self.hasDetails                ? UsageMenuCardLayout.sectionTopPadding                : UsageMenuCardLayout.headerOnlyVerticalPadding)        .padding(            .bottom,            self.hasDetails                ? UsageMenuCardLayout.sectionBottomPadding                : UsageMenuCardLayout.headerOnlyVerticalPadding)        .frame(width: self.width, alignment: .leading)    }     private var hasDetails: Bool {        self.model.hasUsageContent || self.model.usesStackedDetailLayout    }} private struct UsageMenuCardHeaderView: View {    let model: UsageMenuCardView.Model    @Environment(\.menuItemHighlighted) private var isHighlighted     var body: some View {        VStack(alignment: .leading, spacing: UsageMenuCardLayout.headerLineSpacing) {            HStack(alignment: .firstTextBaseline, spacing: UsageMenuCardLayout.headerColumnSpacing) {                Text(self.model.providerName).font(.headline)                    .fontWeight(.semibold)                    .lineLimit(1).truncationMode(.tail).layoutPriority(1)                Spacer()                Text(self.model.email).font(.subheadline)                    .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                    .lineLimit(1).truncationMode(.middle)            }            let subtitleAlignment: VerticalAlignment = self.model.subtitleStyle == .error ? .top : .firstTextBaseline            HStack(alignment: subtitleAlignment, spacing: UsageMenuCardLayout.headerColumnSpacing) {                Text(self.model.subtitleText)                    .font(.footnote)                    .foregroundStyle(self.subtitleColor)                    .lineLimit(self.model.subtitleStyle == .error ? 4 : 1)                    .multilineTextAlignment(.leading)                    .fixedSize(horizontal: false, vertical: true)                    .layoutPriority(1)                    .padding(.bottom, self.model.subtitleStyle == .error ? 4 : 0)                Spacer()                if self.model.subtitleStyle == .error, !self.model.subtitleText.isEmpty {                    CopyIconButton(copyText: self.model.subtitleText, isHighlighted: self.isHighlighted)                }                if let plan = self.model.planText {                    Text(plan)                        .font(.footnote)                        .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                        .lineLimit(1)                }            }        }    }     private var subtitleColor: Color {        switch self.model.subtitleStyle {        case .info: MenuHighlightStyle.secondary(self.isHighlighted)        case .loading: MenuHighlightStyle.secondary(self.isHighlighted)        case .error: MenuHighlightStyle.error(self.isHighlighted)        }    }} private struct CopyIconButtonStyle: ButtonStyle {    let isHighlighted: Bool     func makeBody(configuration: Configuration) -> some View {        configuration.label            .padding(4)            .background {                RoundedRectangle(cornerRadius: 4, style: .continuous)                    .fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(configuration.isPressed ? 0.18 : 0))            }            .scaleEffect(configuration.isPressed ? 0.94 : 1)            .animation(.easeOut(duration: 0.12), value: configuration.isPressed)    }} private struct CopyIconButton: View {    let copyText: String    let isHighlighted: Bool     @State private var didCopy = false    @State private var resetTask: Task<Void, Never>?     var body: some View {        Button {            self.handleCopy()        } label: {            Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc")                .font(.caption2.weight(.semibold))                .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                .frame(width: 18, height: 18)        }        .buttonStyle(CopyIconButtonStyle(isHighlighted: self.isHighlighted))        .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy error"))    }     private func handleCopy() {        let text = self.copyText        self.resetTask?.cancel()        MenuPasteboardCopy.perform(text, completion: {            self.didCopy = true            self.resetTask = Task { @MainActor in                try? await Task.sleep(for: .seconds(0.9))                self.didCopy = false            }        })    }} private struct ProviderCostContent: View {    let section: UsageMenuCardView.Model.ProviderCostSection    let progressColor: Color    @Environment(\.menuItemHighlighted) private var isHighlighted     var body: some View {        VStack(alignment: .leading, spacing: 6) {            Text(self.section.title)                .font(.body)                .fontWeight(.medium)            if let percentUsed = self.section.percentUsed {                UsageProgressBar(                    percent: percentUsed,                    tint: self.progressColor,                    accessibilityLabel: L("Extra usage spent"))            }            HStack(alignment: .firstTextBaseline) {                Text(self.section.spendLine)                    .font(.footnote)                Spacer()                if let percentLine = self.section.percentLine {                    Text(percentLine)                        .font(.footnote)                        .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                }            }        }    }} private struct MetricRow: View {    let metric: UsageMenuCardView.Model.Metric    let title: String    let progressColor: Color    @Environment(\.menuItemHighlighted) private var isHighlighted     var body: some View {        VStack(alignment: .leading, spacing: 6) {            Text(self.title)                .font(.body)                .fontWeight(.medium)            if let statusText = self.metric.statusText {                Text(statusText)                    .font(.footnote)                    .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                    .lineLimit(1)            } else {                UsageProgressBar(                    percent: self.metric.percent,                    tint: self.progressColor,                    accessibilityLabel: self.metric.percentStyle.accessibilityLabel,                    pacePercent: self.metric.pacePercent,                    paceOnTop: self.metric.paceOnTop,                    warningMarkerPercents: self.metric.warningMarkerPercents)                VStack(alignment: .leading, spacing: 2) {                    HStack(alignment: .firstTextBaseline) {                        Text(self.metric.percentLabel)                            .font(.footnote)                            .lineLimit(1)                        Spacer()                        if let rightLabel = self.metric.resetText {                            Text(rightLabel)                                .font(.footnote)                                .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                                .lineLimit(1)                        }                    }                    if self.metric.detailLeftText != nil || self.metric.detailRightText != nil {                        HStack(alignment: .firstTextBaseline) {                            if let detailLeft = self.metric.detailLeftText {                                Text(detailLeft)                                    .font(.footnote)                                    .foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))                                    .lineLimit(1)                            }                            Spacer()                            if let detailRight = self.metric.detailRightText {                                Text(detailRight)                                    .font(.footnote)                                    .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                                    .lineLimit(1)                            }                        }                    }                }                .frame(maxWidth: .infinity, alignment: .leading)                if let detail = self.metric.detailText {                    Text(detail)                        .font(.footnote)                        .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                        .lineLimit(1)                }            }        }        .frame(maxWidth: .infinity, alignment: .leading)        .padding(self.metric.cardStyle ? 10 : 0)        .background(self.metric.cardStyle ? Color.secondary.opacity(self.isHighlighted ? 0.2 : 0.08) : Color.clear)        .clipShape(RoundedRectangle(cornerRadius: self.metric.cardStyle ? 10 : 0))    }} private struct UsageNotesContent: View {    let notes: [String]    @Environment(\.menuItemHighlighted) private var isHighlighted     var body: some View {        VStack(alignment: .leading, spacing: 4) {            ForEach(Array(self.notes.enumerated()), id: \.offset) { _, note in                Text(note)                    .font(.footnote)                    .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))                    .lineLimit(2)                    .fixedSize(horizontal: false, vertical: true)            }        }        .frame(maxWidth: .infinity, alignment: .leading)    }} struct UsageMenuCardHeaderSectionView: View {    let model: UsageMenuCardView.Model    let showDivider: Bool    let width: CGFloat     var body: some View {        VStack(alignment: .leading, spacing: 6) {            UsageMenuCardHeaderView(model: self.model)             if self.showDivider {                Divider()            }        }        .padding(.horizontal, UsageMenuCardLayout.horizontalPadding)        .padding(.top, UsageMenuCardLayout.headerOnlyVerticalPadding)        .padding(.bottom, self.headerBottomPadding)        .frame(width: self.width, alignment: .leading)    }     private var headerBottomPadding: CGFloat {        if self.model.subtitleStyle == .error {            return UsageMenuCardLayout.sectionBottomPadding        }        return self.showDivider            ? UsageMenuCardLayout.sectionBottomPadding            : UsageMenuCardLayout.headerOnlyVerticalPadding    }} struct UsageMenuCardUsageSectionView: View {    let model: UsageMenuCardView.Model

Lines 1–500 of 1520.