From 9538cb7d390294a942f10742174ec946fd5bd066 Mon Sep 17 00:00:00 2001 From: lonewolfyx Date: Thu, 16 Jul 2026 13:35:11 +0800 Subject: [PATCH] fix: size menu bar item to fit content, avoiding truncation The status item used a fixed 76pt width, which could truncate the weekly prefix/percent text when content was wider and leave excess padding when narrower. Replace it with dynamic sizing driven by the view's actual content. MenuBarQuotaView now exposes preferredWidth (computed from the icon, weekly prefix label, percent label, and their spacing) and an intrinsicContentSize override. AppDelegate configures the status item with NSStatusItem.variableLength and syncs statusItem.length to preferredWidth on every snapshot update, so the item always matches its content - no truncation, no extra padding. Also extract layout magic numbers into a Metrics enum and refresh intrinsic size after each update. --- Sources/AgentBar/App/AppDelegate.swift | 25 +++++++++--- Sources/AgentBar/UI/MenuBarQuotaView.swift | 45 ++++++++++++++++------ 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Sources/AgentBar/App/AppDelegate.swift b/Sources/AgentBar/App/AppDelegate.swift index 9750b22..897531b 100644 --- a/Sources/AgentBar/App/AppDelegate.swift +++ b/Sources/AgentBar/App/AppDelegate.swift @@ -5,8 +5,6 @@ import SwiftUI @MainActor final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, SPUUpdaterDelegate { - private static let statusItemWidth: CGFloat = 76 - private let store = QuotaStore() private let updateNotificationService = AppUpdateNotificationService() private var statusItem: NSStatusItem? @@ -43,14 +41,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, SPU } private func configureStatusItem() { - let item = NSStatusBar.system.statusItem(withLength: Self.statusItemWidth) + let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusItem = item guard let button = item.button else { return } - let view = MenuBarQuotaView(frame: NSRect(x: 0, y: 0, width: Self.statusItemWidth, height: NSStatusBar.system.thickness)) + let view = MenuBarQuotaView( + frame: NSRect( + x: 0, + y: 0, + width: 0, + height: NSStatusBar.system.thickness + ) + ) + item.length = view.preferredWidth view.target = self view.action = #selector(togglePopover(_:)) view.translatesAutoresizingMaskIntoConstraints = false @@ -145,10 +151,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, SPU } private func updateMenuBarTitle() { - menuBarView?.update( + guard let menuBarView else { + return + } + + menuBarView.update( snapshot: store.snapshot, statusMessage: store.statusMessage ) + + let preferredWidth = menuBarView.preferredWidth + if statusItem?.length != preferredWidth { + statusItem?.length = preferredWidth + } } private func showCLIUpgradeAlertIfNeeded(_ message: String?) { diff --git a/Sources/AgentBar/UI/MenuBarQuotaView.swift b/Sources/AgentBar/UI/MenuBarQuotaView.swift index d529d7a..41289d7 100644 --- a/Sources/AgentBar/UI/MenuBarQuotaView.swift +++ b/Sources/AgentBar/UI/MenuBarQuotaView.swift @@ -1,10 +1,32 @@ import AppKit final class MenuBarQuotaView: NSControl { + private enum Metrics { + static let horizontalPadding: CGFloat = 6 + static let iconSize: CGFloat = 18 + static let iconSpacing: CGFloat = 5 + static let textSpacing: CGFloat = 5 + } + private let iconView = NSImageView() private let weekPrefixLabel = NSTextField(labelWithString: I18n.current.shortWeek) private let weekPercentLabel = NSTextField(labelWithString: "--") + var preferredWidth: CGFloat { + ceil( + Metrics.horizontalPadding * 2 + + Metrics.iconSize + + Metrics.iconSpacing + + weekPrefixLabel.intrinsicContentSize.width + + Metrics.textSpacing + + weekPercentLabel.intrinsicContentSize.width + ) + } + + override var intrinsicContentSize: NSSize { + NSSize(width: preferredWidth, height: NSStatusBar.system.thickness) + } + override init(frame frameRect: NSRect) { super.init(frame: frameRect) setup() @@ -18,16 +40,17 @@ final class MenuBarQuotaView: NSControl { func update(snapshot: QuotaSnapshot?, statusMessage: String) { toolTip = statusMessage - guard let snapshot else { + if let snapshot { + weekPrefixLabel.stringValue = snapshot.weekly.shortTitle + weekPercentLabel.stringValue = "\(Int(snapshot.weekly.remainingPercent.rounded()))%" + weekPercentLabel.textColor = quotaColor(forRemainingPercent: snapshot.weekly.remainingPercent) + } else { weekPrefixLabel.stringValue = I18n.current.shortWeek weekPercentLabel.stringValue = "--" weekPercentLabel.textColor = .secondaryLabelColor - return } - weekPrefixLabel.stringValue = snapshot.weekly.shortTitle - weekPercentLabel.stringValue = "\(Int(snapshot.weekly.remainingPercent.rounded()))%" - weekPercentLabel.textColor = quotaColor(forRemainingPercent: snapshot.weekly.remainingPercent) + invalidateIntrinsicContentSize() } override func mouseDown(with event: NSEvent) { @@ -69,13 +92,13 @@ final class MenuBarQuotaView: NSControl { NSLayoutConstraint.activate([ heightAnchor.constraint(equalToConstant: NSStatusBar.system.thickness), - iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6), + iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Metrics.horizontalPadding), iconView.centerYAnchor.constraint(equalTo: centerYAnchor), - iconView.widthAnchor.constraint(equalToConstant: 18), - iconView.heightAnchor.constraint(equalToConstant: 18), + iconView.widthAnchor.constraint(equalToConstant: Metrics.iconSize), + iconView.heightAnchor.constraint(equalToConstant: Metrics.iconSize), - weekRow.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 5), - weekRow.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6), + weekRow.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: Metrics.iconSpacing), + weekRow.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Metrics.horizontalPadding), weekRow.centerYAnchor.constraint(equalTo: centerYAnchor), ]) } @@ -85,7 +108,7 @@ final class MenuBarQuotaView: NSControl { row.orientation = .horizontal row.alignment = .centerY row.distribution = .gravityAreas - row.spacing = 5 + row.spacing = Metrics.textSpacing row.translatesAutoresizingMaskIntoConstraints = false return row