diff --git a/Sources/CodexRunway/StatusBarContentView.swift b/Sources/CodexRunway/StatusBarContentView.swift index 1e74da7..29916d0 100644 --- a/Sources/CodexRunway/StatusBarContentView.swift +++ b/Sources/CodexRunway/StatusBarContentView.swift @@ -68,7 +68,7 @@ final class StatusBarContentView: NSView { super.draw(dirtyRect) switch state.configuration.style { case .text: - drawText() + break // Percentage text is rendered by the native status bar button. case .countdown: drawCountdown() case .battery: @@ -137,14 +137,6 @@ final class StatusBarContentView: NSView { // MARK: - Countdown / meters / rings - private func drawText() { - let captions = layout.textCaptions - let frames = layout.columnFrames(widths: layout.textColumnWidths, gap: 4, in: bounds) - for (caption, frame) in zip(captions, frames) { - drawCentered(caption, font: layout.textFont, rect: frame, color: .labelColor) - } - } - private func drawCountdown() { guard renderPlan.meters.count > 1 else { drawCentered(state.content.text, font: layout.countdownFont, rect: bounds, color: .labelColor) diff --git a/Sources/CodexRunway/StatusControllerStatusBar.swift b/Sources/CodexRunway/StatusControllerStatusBar.swift index 04290a4..6895b77 100644 --- a/Sources/CodexRunway/StatusControllerStatusBar.swift +++ b/Sources/CodexRunway/StatusControllerStatusBar.swift @@ -26,9 +26,7 @@ extension StatusController { text: model.selectedStatusText, meters: model.selectedQuotaMeters, displayMinute: Int(Date().timeIntervalSince1970 / 60))) - let didChange = statusBarView.update(state) - guard didChange else { return } - statusItem.length = statusBarView.preferredWidth + guard Self.updateStatusBarContent(state, statusItem: statusItem, contentView: statusBarView) else { return } let quotaDetails = model.selectedQuotaMeters .map { "\($0.title): \($0.remainingPercent)%" } .joined(separator: " · ") @@ -36,4 +34,21 @@ extension StatusController { ? "CodexRunway · \(model.selectedStatusText)" : "CodexRunway · \(model.selectedStatusText)\n\(quotaDetails)" } + + @discardableResult + static func updateStatusBarContent( + _ state: StatusBarContentState, + statusItem: NSStatusItem, + contentView: StatusBarContentView) -> Bool + { + guard contentView.update(state) else { return false } + let layout = StatusBarContentLayout(state: state) + let usesNativeText = state.configuration.style == .text + contentView.isHidden = usesNativeText + // Let AppKit render the title's color and inactive-menu-bar appearance. + statusItem.button?.font = layout.textFont + statusItem.button?.title = usesNativeText ? layout.textCaptions.joined(separator: " ") : "" + statusItem.length = usesNativeText ? NSStatusItem.variableLength : contentView.preferredWidth + return true + } } diff --git a/Tests/CodexRunwayTests/StatusBarRenderPlanTests.swift b/Tests/CodexRunwayTests/StatusBarRenderPlanTests.swift index 1254f0c..860872e 100644 --- a/Tests/CodexRunwayTests/StatusBarRenderPlanTests.swift +++ b/Tests/CodexRunwayTests/StatusBarRenderPlanTests.swift @@ -187,9 +187,9 @@ struct StatusBarRenderPlanTests { #expect(layout.preferredWidth == columnWidth) } - @Test("all status bar styles render weekly-only and model-specific layouts") + @Test("custom-drawn status bar styles render weekly-only and model-specific layouts") @MainActor - func allStylesRenderVariableQuotaCounts() throws { + func customStylesRenderVariableQuotaCounts() throws { let weekly = meter(title: "每周", usedPercent: 11, windowMinutes: 10_080) let fiveHour = meter(title: "5小时", usedPercent: 20, windowMinutes: 300) let modelSpecific = meter( @@ -198,7 +198,7 @@ struct StatusBarRenderPlanTests { windowMinutes: 10_080, source: .modelSpecific) - for style in StatusBarDisplayStyle.allCases { + for style in StatusBarDisplayStyle.allCases where style != .text { let stylePreferences = preferences(style: style) let compact = try render(preferences: stylePreferences, meters: [weekly]) let expanded = try render( @@ -213,6 +213,46 @@ struct StatusBarRenderPlanTests { } } + @Test("native percentage titles update and switch back to custom-drawn styles") + @MainActor + func nativePercentageTitlesFollowState() throws { + _ = NSApplication.shared + let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + defer { NSStatusBar.system.removeStatusItem(statusItem) } + let button = try #require(statusItem.button) + let view = StatusBarContentView(frame: .zero) + button.addSubview(view) + let weekly = meter(title: "每周", usedPercent: 0, windowMinutes: 10_080) + let fiveHour = meter(title: "5小时", usedPercent: 20, windowMinutes: 300) + let modelSpecific = meter( + title: "模型配额", + usedPercent: 11, + windowMinutes: 10_080, + source: .modelSpecific) + + for (meters, title) in [([weekly], "100%"), ([fiveHour, weekly, modelSpecific], "80% 100% 89%"), ([], "--")] { + let textState = StatusBarContentState( + configuration: .init(preferences: preferences(style: .text), language: .simplifiedChinese), + content: .init(text: "6天", meters: meters, displayMinute: 0)) + for style in StatusBarDisplayStyle.allCases where style != .text { + #expect(StatusController.updateStatusBarContent(textState, statusItem: statusItem, contentView: view)) + #expect(button.title == title) + #expect(button.font == NSFont.systemFont(ofSize: 14, weight: .semibold)) + #expect(view.isHidden) + #expect(statusItem.length == NSStatusItem.variableLength) + #expect(!StatusController.updateStatusBarContent(textState, statusItem: statusItem, contentView: view)) + + let customState = StatusBarContentState( + configuration: .init(preferences: preferences(style: style), language: .simplifiedChinese), + content: textState.content) + #expect(StatusController.updateStatusBarContent(customState, statusItem: statusItem, contentView: view)) + #expect(button.title.isEmpty) + #expect(!view.isHidden) + #expect(statusItem.length == view.preferredWidth) + } + } + } + @Test("text style displays the same percentages as quota rings") func textStyleUsesRingPercentages() { let fiveHour = meter(title: "5小时", usedPercent: 20, windowMinutes: 300)