From 3bb33bdeed5bc4e55f6977718506d5112cfff4c5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 00:19:29 +0000 Subject: [PATCH] fix(mac): render context bar as image --- .../Sources/Clawdis/ContextUsageBar.swift | 68 ++++++++++++++++--- .../Sources/Clawdis/MenuContentView.swift | 4 +- .../Sources/Clawdis/SessionsSettings.swift | 6 +- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/apps/macos/Sources/Clawdis/ContextUsageBar.swift b/apps/macos/Sources/Clawdis/ContextUsageBar.swift index 755511dce..6c1a2d497 100644 --- a/apps/macos/Sources/Clawdis/ContextUsageBar.swift +++ b/apps/macos/Sources/Clawdis/ContextUsageBar.swift @@ -1,8 +1,10 @@ +import AppKit import SwiftUI struct ContextUsageBar: View { let usedTokens: Int let contextTokens: Int + var width: CGFloat = 220 var height: CGFloat = 6 private var clampedFractionUsed: Double { @@ -24,17 +26,16 @@ struct ContextUsageBar: View { } var body: some View { - // Prefer the native progress indicator in menus; `GeometryReader` can get wonky - // inside `MenuBarExtra`-backed menus (often receiving zero width). - ZStack { - Capsule() - .fill(Color.secondary.opacity(0.25)) - ProgressView(value: self.clampedFractionUsed, total: 1) - .progressViewStyle(.linear) - .tint(self.tint) - .clipShape(Capsule()) - } - .frame(height: self.height) + // SwiftUI menus (MenuBarExtraStyle.menu) drop certain view types (including ProgressView/Canvas). + // Render the bar as an image to reliably display inside the menu. + Image(nsImage: Self.renderBar( + width: self.width, + height: self.height, + fractionUsed: self.clampedFractionUsed, + percentUsed: self.percentUsed)) + .resizable() + .interpolation(.none) + .frame(width: self.width, height: self.height) .accessibilityLabel("Context usage") .accessibilityValue(self.accessibilityValue) } @@ -44,4 +45,49 @@ struct ContextUsageBar: View { let pct = Int(round(self.clampedFractionUsed * 100)) return "\(pct) percent used" } + + private static func renderBar( + width: CGFloat, + height: CGFloat, + fractionUsed: Double, + percentUsed: Int?) -> NSImage + { + let clamped = min(1, max(0, fractionUsed)) + let size = NSSize(width: max(1, width), height: max(1, height)) + let image = NSImage(size: size) + image.isTemplate = false + + image.lockFocus() + defer { image.unlockFocus() } + + let rect = NSRect(origin: .zero, size: size) + let radius = rect.height / 2 + + let background = NSColor.white.withAlphaComponent(0.12) + let stroke = NSColor.white.withAlphaComponent(0.18) + + let fill: NSColor = { + guard let pct = percentUsed else { return NSColor.secondaryLabelColor } + if pct >= 95 { return .systemRed } + if pct >= 80 { return .systemOrange } + if pct >= 60 { return .systemYellow } + return .systemGreen + }() + + let track = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius) + background.setFill() + track.fill() + stroke.setStroke() + track.lineWidth = 0.75 + track.stroke() + + let fillWidth = max(1, floor(rect.width * clamped)) + let fillRect = NSRect(x: rect.minX, y: rect.minY, width: fillWidth, height: rect.height) + let clip = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius) + clip.addClip() + fill.setFill() + NSBezierPath(rect: fillRect).fill() + + return image + } } diff --git a/apps/macos/Sources/Clawdis/MenuContentView.swift b/apps/macos/Sources/Clawdis/MenuContentView.swift index 3f4b9f462..5ae855e45 100644 --- a/apps/macos/Sources/Clawdis/MenuContentView.swift +++ b/apps/macos/Sources/Clawdis/MenuContentView.swift @@ -265,8 +265,8 @@ struct MenuContent: View { } ContextUsageBar( usedTokens: row.tokens.total, - contextTokens: row.tokens.contextTokens) - .frame(width: 220) + contextTokens: row.tokens.contextTokens, + width: 220) } .padding(.vertical, 2) } else { diff --git a/apps/macos/Sources/Clawdis/SessionsSettings.swift b/apps/macos/Sources/Clawdis/SessionsSettings.swift index 5c44bc107..78cb47671 100644 --- a/apps/macos/Sources/Clawdis/SessionsSettings.swift +++ b/apps/macos/Sources/Clawdis/SessionsSettings.swift @@ -151,8 +151,10 @@ struct SessionsSettings: View { .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } - ContextUsageBar(usedTokens: row.tokens.total, contextTokens: row.tokens.contextTokens) - .frame(maxWidth: .infinity) + ContextUsageBar( + usedTokens: row.tokens.total, + contextTokens: row.tokens.contextTokens, + width: 260) } HStack(spacing: 10) {