import AppKitimport CodexBarCore final class ProviderSwitcherView: NSView { private struct Segment { let provider: UsageProvider let image: NSImage let title: String } private struct WeeklyIndicator { let provider: UsageProvider let track: NSView let fill: NSView } private let segments: [Segment] private let onSelect: (UsageProvider) -> Void private let showsIcons: Bool private let weeklyRemainingProvider: (UsageProvider) -> Double? private var buttons: [NSButton] = [] private var weeklyIndicators: [ObjectIdentifier: WeeklyIndicator] = [:] private var hoverTrackingArea: NSTrackingArea? private var segmentWidths: [CGFloat] = [] private let selectedBackground = NSColor.controlAccentColor.cgColor private let unselectedBackground = NSColor.clear.cgColor private let selectedTextColor = NSColor.white private let unselectedTextColor = NSColor.secondaryLabelColor private let stackedIcons: Bool private let useTwoRows: Bool private let rowSpacing: CGFloat private let rowHeight: CGFloat private var preferredWidth: CGFloat = 0 private var hoveredButtonTag: Int? private let lightModeOverlayLayer = CALayer() init( providers: [UsageProvider], selected: UsageProvider?, width: CGFloat, showsIcons: Bool, iconProvider: (UsageProvider) -> NSImage, weeklyRemainingProvider: @escaping (UsageProvider) -> Double?, onSelect: @escaping (UsageProvider) -> Void) { let minimumGap: CGFloat = 1 self.segments = providers.map { provider in let fullTitle = Self.switcherTitle(for: provider) let icon = iconProvider(provider) icon.isTemplate = true // Avoid any resampling: we ship exact 16pt/32px assets for crisp rendering. icon.size = NSSize(width: 16, height: 16) return Segment( provider: provider, image: icon, title: fullTitle) } self.onSelect = onSelect self.showsIcons = showsIcons self.weeklyRemainingProvider = weeklyRemainingProvider self.stackedIcons = showsIcons && providers.count > 3 let initialOuterPadding = Self.switcherOuterPadding( for: width, count: self.segments.count, minimumGap: minimumGap) let initialMaxAllowedSegmentWidth = Self.maxAllowedUniformSegmentWidth( for: width, count: self.segments.count, outerPadding: initialOuterPadding, minimumGap: minimumGap) self.useTwoRows = Self.shouldUseTwoRows( count: self.segments.count, maxAllowedSegmentWidth: initialMaxAllowedSegmentWidth, stackedIcons: self.stackedIcons) self.rowSpacing = self.stackedIcons ? 4 : 2 self.rowHeight = self.stackedIcons ? 36 : 30 let height: CGFloat = self.useTwoRows ? (self.rowHeight * 2 + self.rowSpacing) : self.rowHeight self.preferredWidth = width super.init(frame: NSRect(x: 0, y: 0, width: width, height: height)) Self.clearButtonWidthCache() self.wantsLayer = true self.layer?.masksToBounds = false self.lightModeOverlayLayer.masksToBounds = false self.layer?.insertSublayer(self.lightModeOverlayLayer, at: 0) self.updateLightModeStyling() let layoutCount = self.useTwoRows ? Int(ceil(Double(self.segments.count) / 2.0)) : self.segments.count let outerPadding: CGFloat = Self.switcherOuterPadding( for: width, count: layoutCount, minimumGap: minimumGap) let maxAllowedSegmentWidth = Self.maxAllowedUniformSegmentWidth( for: width, count: layoutCount, outerPadding: outerPadding, minimumGap: minimumGap) func makeButton(index: Int, segment: Segment) -> NSButton { let button: NSButton if self.stackedIcons { let stacked = StackedToggleButton( title: segment.title, image: segment.image, target: self, action: #selector(self.handleSelection(_:))) button = stacked } else if self.showsIcons { let inline = InlineIconToggleButton( title: segment.title, image: segment.image, target: self, action: #selector(self.handleSelection(_:))) button = inline } else { button = PaddedToggleButton( title: segment.title, target: self, action: #selector(self.handleSelection(_:))) } button.tag = index if self.showsIcons { if self.stackedIcons { // StackedToggleButton manages its own image view. } else { // InlineIconToggleButton manages its own image view. } } else { button.image = nil button.imagePosition = .noImage } let remaining = self.weeklyRemainingProvider(segment.provider) self.addWeeklyIndicator(to: button, provider: segment.provider, remainingPercent: remaining) button.bezelStyle = .regularSquare button.isBordered = false button.controlSize = .small button.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) button.setButtonType(.toggle) button.contentTintColor = self.unselectedTextColor button.alignment = .center button.wantsLayer = true button.layer?.cornerRadius = 6 button.state = (selected == segment.provider) ? .on : .off button.toolTip = nil button.translatesAutoresizingMaskIntoConstraints = false self.buttons.append(button) return button } for (index, segment) in self.segments.enumerated() { let button = makeButton(index: index, segment: segment) self.addSubview(button) } let uniformWidth: CGFloat if self.useTwoRows || !self.stackedIcons { uniformWidth = self.applyUniformSegmentWidth(maxAllowedWidth: maxAllowedSegmentWidth) if uniformWidth > 0 { self.segmentWidths = Array(repeating: uniformWidth, count: self.buttons.count) } } else { self.segmentWidths = self.applyNonUniformSegmentWidths( totalWidth: width, outerPadding: outerPadding, minimumGap: minimumGap) uniformWidth = 0 } self.applyLayout( outerPadding: outerPadding, minimumGap: minimumGap, uniformWidth: uniformWidth) if width > 0 { self.preferredWidth = width self.frame.size.width = width } self.updateButtonStyles() } override func layout() { super.layout() self.lightModeOverlayLayer.frame = self.bounds } override func viewDidChangeEffectiveAppearance() { super.viewDidChangeEffectiveAppearance() self.updateLightModeStyling() self.updateButtonStyles() } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() self.window?.acceptsMouseMovedEvents = true } override func updateTrackingAreas() { super.updateTrackingAreas() if let hoverTrackingArea { self.removeTrackingArea(hoverTrackingArea) } let trackingArea = NSTrackingArea( rect: .zero, options: [ .activeAlways, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved, ], owner: self, userInfo: nil) self.addTrackingArea(trackingArea) self.hoverTrackingArea = trackingArea } override func mouseMoved(with event: NSEvent) { let location = self.convert(event.locationInWindow, from: nil) let hoveredTag = self.buttons.first(where: { $0.frame.contains(location) })?.tag guard hoveredTag != self.hoveredButtonTag else { return } self.hoveredButtonTag = hoveredTag self.updateButtonStyles() } override func mouseExited(with event: NSEvent) { guard self.hoveredButtonTag != nil else { return } self.hoveredButtonTag = nil self.updateButtonStyles() } private func applyLayout( outerPadding: CGFloat, minimumGap: CGFloat, uniformWidth: CGFloat) { if self.useTwoRows { self.applyTwoRowLayout( outerPadding: outerPadding, minimumGap: minimumGap, uniformWidth: uniformWidth) return } if self.buttons.count == 2 { let left = self.buttons[0] let right = self.buttons[1] let gap = right.leadingAnchor.constraint(greaterThanOrEqualTo: left.trailingAnchor, constant: minimumGap) gap.priority = .defaultHigh NSLayoutConstraint.activate([ left.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), left.centerYAnchor.constraint(equalTo: self.centerYAnchor), right.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), right.centerYAnchor.constraint(equalTo: self.centerYAnchor), gap, ]) return } if self.buttons.count == 3 { let left = self.buttons[0] let mid = self.buttons[1] let right = self.buttons[2] let leftGap = mid.leadingAnchor.constraint(greaterThanOrEqualTo: left.trailingAnchor, constant: minimumGap) leftGap.priority = .defaultHigh let rightGap = right.leadingAnchor.constraint( greaterThanOrEqualTo: mid.trailingAnchor, constant: minimumGap) rightGap.priority = .defaultHigh NSLayoutConstraint.activate([ left.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), left.centerYAnchor.constraint(equalTo: self.centerYAnchor), mid.centerXAnchor.constraint(equalTo: self.centerXAnchor), mid.centerYAnchor.constraint(equalTo: self.centerYAnchor), right.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), right.centerYAnchor.constraint(equalTo: self.centerYAnchor), leftGap, rightGap, ]) return } if self.buttons.count >= 4 { let widths = self.segmentWidths.isEmpty ? self.buttons.map { ceil($0.fittingSize.width) } : self.segmentWidths let layoutWidth = self.preferredWidth > 0 ? self.preferredWidth : self.bounds.width let availableWidth = max(0, layoutWidth - outerPadding * 2) let gaps = max(1, widths.count - 1) let computedGap = gaps > 0 ? max(minimumGap, (availableWidth - widths.reduce(0, +)) / CGFloat(gaps)) : 0 let rowContainer = NSView() rowContainer.translatesAutoresizingMaskIntoConstraints = false self.addSubview(rowContainer) NSLayoutConstraint.activate([ rowContainer.topAnchor.constraint(equalTo: self.topAnchor), rowContainer.bottomAnchor.constraint(equalTo: self.bottomAnchor), rowContainer.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), rowContainer.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), ]) var xOffset: CGFloat = 0 for (index, button) in self.buttons.enumerated() { let width = index < widths.count ? widths[index] : 0 if self.stackedIcons { NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: rowContainer.leadingAnchor, constant: xOffset), button.topAnchor.constraint(equalTo: rowContainer.topAnchor), ]) } else { NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: rowContainer.leadingAnchor, constant: xOffset), button.centerYAnchor.constraint(equalTo: rowContainer.centerYAnchor), ]) } xOffset += width + computedGap } return } if let first = self.buttons.first { NSLayoutConstraint.activate([ first.centerXAnchor.constraint(equalTo: self.centerXAnchor), first.centerYAnchor.constraint(equalTo: self.centerYAnchor), ]) } } private func applyTwoRowLayout( outerPadding: CGFloat, minimumGap: CGFloat, uniformWidth: CGFloat) { let splitIndex = Int(ceil(Double(self.buttons.count) / 2.0)) let topButtons = Array(self.buttons.prefix(splitIndex)) let bottomButtons = Array(self.buttons.dropFirst(splitIndex)) let columns = max(topButtons.count, bottomButtons.count) let layoutWidth = self.preferredWidth > 0 ? self.preferredWidth : self.bounds.width let availableWidth = max(0, layoutWidth - outerPadding * 2) let gaps = max(1, columns - 1) let totalWidth = uniformWidth * CGFloat(columns) let computedGap = gaps > 0 ? max(minimumGap, (availableWidth - totalWidth) / CGFloat(gaps)) : 0 let gridContainer = NSView() gridContainer.translatesAutoresizingMaskIntoConstraints = false self.addSubview(gridContainer) NSLayoutConstraint.activate([ gridContainer.topAnchor.constraint(equalTo: self.topAnchor), gridContainer.bottomAnchor.constraint(equalTo: self.bottomAnchor), gridContainer.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), gridContainer.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), ]) let topRow = NSView() topRow.translatesAutoresizingMaskIntoConstraints = false gridContainer.addSubview(topRow) let bottomRow = NSView() bottomRow.translatesAutoresizingMaskIntoConstraints = false gridContainer.addSubview(bottomRow) NSLayoutConstraint.activate([ topRow.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor), topRow.trailingAnchor.constraint(equalTo: gridContainer.trailingAnchor), topRow.topAnchor.constraint(equalTo: gridContainer.topAnchor), topRow.heightAnchor.constraint(equalToConstant: self.rowHeight), bottomRow.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor), bottomRow.trailingAnchor.constraint(equalTo: gridContainer.trailingAnchor), bottomRow.bottomAnchor.constraint(equalTo: gridContainer.bottomAnchor), bottomRow.heightAnchor.constraint(equalToConstant: self.rowHeight), bottomRow.topAnchor.constraint(equalTo: topRow.bottomAnchor, constant: self.rowSpacing), ]) for index in 0..<columns { let xOffset = CGFloat(index) * (uniformWidth + computedGap) if index < topButtons.count { let button = topButtons[index] NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor, constant: xOffset), button.centerYAnchor.constraint(equalTo: topRow.centerYAnchor), ]) } if index < bottomButtons.count { let button = bottomButtons[index] NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor, constant: xOffset), button.centerYAnchor.constraint(equalTo: bottomRow.centerYAnchor), ]) } } } private static func shouldUseTwoRows( count: Int, maxAllowedSegmentWidth: CGFloat, stackedIcons: Bool) -> Bool { guard count > 1 else { return false } let minimumComfortableAverage: CGFloat = stackedIcons ? 44 : 54 return maxAllowedSegmentWidth < minimumComfortableAverage } private static func switcherOuterPadding(for width: CGFloat, count: Int, minimumGap: CGFloat) -> CGFloat { // Align with the card's left/right content grid when possible. let preferred: CGFloat = 16 let reduced: CGFloat = 10 let minimal: CGFloat = 6 func averageButtonWidth(outerPadding: CGFloat) -> CGFloat { let available = width - outerPadding * 2 - minimumGap * CGFloat(max(0, count - 1)) guard count > 0 else { return 0 } return available / CGFloat(count) } // Only sacrifice padding when we'd otherwise squeeze buttons into unreadable widths. let minimumComfortableAverage: CGFloat = count >= 5 ? 46 : 54 if averageButtonWidth(outerPadding: preferred) >= minimumComfortableAverage { return preferred } if averageButtonWidth(outerPadding: reduced) >= minimumComfortableAverage { return reduced } return minimal } @available(*, unavailable) required init?(coder: NSCoder) { nil } override var intrinsicContentSize: NSSize { NSSize(width: self.preferredWidth, height: self.frame.size.height) } @objc private func handleSelection(_ sender: NSButton) { let index = sender.tag guard self.segments.indices.contains(index) else { return } for (idx, button) in self.buttons.enumerated() { button.state = (idx == index) ? .on : .off } self.updateButtonStyles() self.onSelect(self.segments[index].provider) } private func updateButtonStyles() { for button in self.buttons { let isSelected = button.state == .on let isHovered = self.hoveredButtonTag == button.tag button.contentTintColor = isSelected ? self.selectedTextColor : self.unselectedTextColor button.layer?.backgroundColor = if isSelected { self.selectedBackground } else if isHovered { self.hoverPlateColor() } else { self.unselectedBackground } self.updateWeeklyIndicatorVisibility(for: button) (button as? StackedToggleButton)?.setContentTintColor(button.contentTintColor) (button as? InlineIconToggleButton)?.setContentTintColor(button.contentTintColor) } } private func isLightMode() -> Bool { self.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .aqua } private func updateLightModeStyling() { guard self.isLightMode() else { self.lightModeOverlayLayer.backgroundColor = nil return } // The menu card background is very bright in light mode; add a subtle neutral wash to ground the switcher. self.lightModeOverlayLayer.backgroundColor = NSColor.black.withAlphaComponent(0.035).cgColor } private func hoverPlateColor() -> CGColor { if self.isLightMode() { return NSColor.black.withAlphaComponent(0.095).cgColor } return NSColor.labelColor.withAlphaComponent(0.06).cgColor } // Cache for button width measurements to avoid repeated layout passes. private static var buttonWidthCache: [ObjectIdentifier: CGFloat] = [:] private static func maxToggleWidth(for button: NSButton) -> CGFloat { let buttonId = ObjectIdentifier(button) // Return cached value if available. if let cached = buttonWidthCache[buttonId] { return cached } let originalState = button.stateLines 1–500 of 817. Load more
import AppKit import CodexBarCore final class ProviderSwitcherView: NSView { private struct Segment { let provider: UsageProvider let image: NSImage let title: String } private struct WeeklyIndicator { let provider: UsageProvider let track: NSView let fill: NSView } private let segments: [Segment] private let onSelect: (UsageProvider) -> Void private let showsIcons: Bool private let weeklyRemainingProvider: (UsageProvider) -> Double? private var buttons: [NSButton] = [] private var weeklyIndicators: [ObjectIdentifier: WeeklyIndicator] = [:] private var hoverTrackingArea: NSTrackingArea? private var segmentWidths: [CGFloat] = [] private let selectedBackground = NSColor.controlAccentColor.cgColor private let unselectedBackground = NSColor.clear.cgColor private let selectedTextColor = NSColor.white private let unselectedTextColor = NSColor.secondaryLabelColor private let stackedIcons: Bool private let useTwoRows: Bool private let rowSpacing: CGFloat private let rowHeight: CGFloat private var preferredWidth: CGFloat = 0 private var hoveredButtonTag: Int? private let lightModeOverlayLayer = CALayer() init( providers: [UsageProvider], selected: UsageProvider?, width: CGFloat, showsIcons: Bool, iconProvider: (UsageProvider) -> NSImage, weeklyRemainingProvider: @escaping (UsageProvider) -> Double?, onSelect: @escaping (UsageProvider) -> Void) { let minimumGap: CGFloat = 1 self.segments = providers.map { provider in let fullTitle = Self.switcherTitle(for: provider) let icon = iconProvider(provider) icon.isTemplate = true // Avoid any resampling: we ship exact 16pt/32px assets for crisp rendering. icon.size = NSSize(width: 16, height: 16) return Segment( provider: provider, image: icon, title: fullTitle) } self.onSelect = onSelect self.showsIcons = showsIcons self.weeklyRemainingProvider = weeklyRemainingProvider self.stackedIcons = showsIcons && providers.count > 3 let initialOuterPadding = Self.switcherOuterPadding( for: width, count: self.segments.count, minimumGap: minimumGap) let initialMaxAllowedSegmentWidth = Self.maxAllowedUniformSegmentWidth( for: width, count: self.segments.count, outerPadding: initialOuterPadding, minimumGap: minimumGap) self.useTwoRows = Self.shouldUseTwoRows( count: self.segments.count, maxAllowedSegmentWidth: initialMaxAllowedSegmentWidth, stackedIcons: self.stackedIcons) self.rowSpacing = self.stackedIcons ? 4 : 2 self.rowHeight = self.stackedIcons ? 36 : 30 let height: CGFloat = self.useTwoRows ? (self.rowHeight * 2 + self.rowSpacing) : self.rowHeight self.preferredWidth = width super.init(frame: NSRect(x: 0, y: 0, width: width, height: height)) Self.clearButtonWidthCache() self.wantsLayer = true self.layer?.masksToBounds = false self.lightModeOverlayLayer.masksToBounds = false self.layer?.insertSublayer(self.lightModeOverlayLayer, at: 0) self.updateLightModeStyling() let layoutCount = self.useTwoRows ? Int(ceil(Double(self.segments.count) / 2.0)) : self.segments.count let outerPadding: CGFloat = Self.switcherOuterPadding( for: width, count: layoutCount, minimumGap: minimumGap) let maxAllowedSegmentWidth = Self.maxAllowedUniformSegmentWidth( for: width, count: layoutCount, outerPadding: outerPadding, minimumGap: minimumGap) func makeButton(index: Int, segment: Segment) -> NSButton { let button: NSButton if self.stackedIcons { let stacked = StackedToggleButton( title: segment.title, image: segment.image, target: self, action: #selector(self.handleSelection(_:))) button = stacked } else if self.showsIcons { let inline = InlineIconToggleButton( title: segment.title, image: segment.image, target: self, action: #selector(self.handleSelection(_:))) button = inline } else { button = PaddedToggleButton( title: segment.title, target: self, action: #selector(self.handleSelection(_:))) } button.tag = index if self.showsIcons { if self.stackedIcons { // StackedToggleButton manages its own image view. } else { // InlineIconToggleButton manages its own image view. } } else { button.image = nil button.imagePosition = .noImage } let remaining = self.weeklyRemainingProvider(segment.provider) self.addWeeklyIndicator(to: button, provider: segment.provider, remainingPercent: remaining) button.bezelStyle = .regularSquare button.isBordered = false button.controlSize = .small button.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) button.setButtonType(.toggle) button.contentTintColor = self.unselectedTextColor button.alignment = .center button.wantsLayer = true button.layer?.cornerRadius = 6 button.state = (selected == segment.provider) ? .on : .off button.toolTip = nil button.translatesAutoresizingMaskIntoConstraints = false self.buttons.append(button) return button } for (index, segment) in self.segments.enumerated() { let button = makeButton(index: index, segment: segment) self.addSubview(button) } let uniformWidth: CGFloat if self.useTwoRows || !self.stackedIcons { uniformWidth = self.applyUniformSegmentWidth(maxAllowedWidth: maxAllowedSegmentWidth) if uniformWidth > 0 { self.segmentWidths = Array(repeating: uniformWidth, count: self.buttons.count) } } else { self.segmentWidths = self.applyNonUniformSegmentWidths( totalWidth: width, outerPadding: outerPadding, minimumGap: minimumGap) uniformWidth = 0 } self.applyLayout( outerPadding: outerPadding, minimumGap: minimumGap, uniformWidth: uniformWidth) if width > 0 { self.preferredWidth = width self.frame.size.width = width } self.updateButtonStyles() } override func layout() { super.layout() self.lightModeOverlayLayer.frame = self.bounds } override func viewDidChangeEffectiveAppearance() { super.viewDidChangeEffectiveAppearance() self.updateLightModeStyling() self.updateButtonStyles() } override func viewDidMoveToWindow() { super.viewDidMoveToWindow() self.window?.acceptsMouseMovedEvents = true } override func updateTrackingAreas() { super.updateTrackingAreas() if let hoverTrackingArea { self.removeTrackingArea(hoverTrackingArea) } let trackingArea = NSTrackingArea( rect: .zero, options: [ .activeAlways, .inVisibleRect, .mouseEnteredAndExited, .mouseMoved, ], owner: self, userInfo: nil) self.addTrackingArea(trackingArea) self.hoverTrackingArea = trackingArea } override func mouseMoved(with event: NSEvent) { let location = self.convert(event.locationInWindow, from: nil) let hoveredTag = self.buttons.first(where: { $0.frame.contains(location) })?.tag guard hoveredTag != self.hoveredButtonTag else { return } self.hoveredButtonTag = hoveredTag self.updateButtonStyles() } override func mouseExited(with event: NSEvent) { guard self.hoveredButtonTag != nil else { return } self.hoveredButtonTag = nil self.updateButtonStyles() } private func applyLayout( outerPadding: CGFloat, minimumGap: CGFloat, uniformWidth: CGFloat) { if self.useTwoRows { self.applyTwoRowLayout( outerPadding: outerPadding, minimumGap: minimumGap, uniformWidth: uniformWidth) return } if self.buttons.count == 2 { let left = self.buttons[0] let right = self.buttons[1] let gap = right.leadingAnchor.constraint(greaterThanOrEqualTo: left.trailingAnchor, constant: minimumGap) gap.priority = .defaultHigh NSLayoutConstraint.activate([ left.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), left.centerYAnchor.constraint(equalTo: self.centerYAnchor), right.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), right.centerYAnchor.constraint(equalTo: self.centerYAnchor), gap, ]) return } if self.buttons.count == 3 { let left = self.buttons[0] let mid = self.buttons[1] let right = self.buttons[2] let leftGap = mid.leadingAnchor.constraint(greaterThanOrEqualTo: left.trailingAnchor, constant: minimumGap) leftGap.priority = .defaultHigh let rightGap = right.leadingAnchor.constraint( greaterThanOrEqualTo: mid.trailingAnchor, constant: minimumGap) rightGap.priority = .defaultHigh NSLayoutConstraint.activate([ left.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), left.centerYAnchor.constraint(equalTo: self.centerYAnchor), mid.centerXAnchor.constraint(equalTo: self.centerXAnchor), mid.centerYAnchor.constraint(equalTo: self.centerYAnchor), right.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), right.centerYAnchor.constraint(equalTo: self.centerYAnchor), leftGap, rightGap, ]) return } if self.buttons.count >= 4 { let widths = self.segmentWidths.isEmpty ? self.buttons.map { ceil($0.fittingSize.width) } : self.segmentWidths let layoutWidth = self.preferredWidth > 0 ? self.preferredWidth : self.bounds.width let availableWidth = max(0, layoutWidth - outerPadding * 2) let gaps = max(1, widths.count - 1) let computedGap = gaps > 0 ? max(minimumGap, (availableWidth - widths.reduce(0, +)) / CGFloat(gaps)) : 0 let rowContainer = NSView() rowContainer.translatesAutoresizingMaskIntoConstraints = false self.addSubview(rowContainer) NSLayoutConstraint.activate([ rowContainer.topAnchor.constraint(equalTo: self.topAnchor), rowContainer.bottomAnchor.constraint(equalTo: self.bottomAnchor), rowContainer.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), rowContainer.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), ]) var xOffset: CGFloat = 0 for (index, button) in self.buttons.enumerated() { let width = index < widths.count ? widths[index] : 0 if self.stackedIcons { NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: rowContainer.leadingAnchor, constant: xOffset), button.topAnchor.constraint(equalTo: rowContainer.topAnchor), ]) } else { NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: rowContainer.leadingAnchor, constant: xOffset), button.centerYAnchor.constraint(equalTo: rowContainer.centerYAnchor), ]) } xOffset += width + computedGap } return } if let first = self.buttons.first { NSLayoutConstraint.activate([ first.centerXAnchor.constraint(equalTo: self.centerXAnchor), first.centerYAnchor.constraint(equalTo: self.centerYAnchor), ]) } } private func applyTwoRowLayout( outerPadding: CGFloat, minimumGap: CGFloat, uniformWidth: CGFloat) { let splitIndex = Int(ceil(Double(self.buttons.count) / 2.0)) let topButtons = Array(self.buttons.prefix(splitIndex)) let bottomButtons = Array(self.buttons.dropFirst(splitIndex)) let columns = max(topButtons.count, bottomButtons.count) let layoutWidth = self.preferredWidth > 0 ? self.preferredWidth : self.bounds.width let availableWidth = max(0, layoutWidth - outerPadding * 2) let gaps = max(1, columns - 1) let totalWidth = uniformWidth * CGFloat(columns) let computedGap = gaps > 0 ? max(minimumGap, (availableWidth - totalWidth) / CGFloat(gaps)) : 0 let gridContainer = NSView() gridContainer.translatesAutoresizingMaskIntoConstraints = false self.addSubview(gridContainer) NSLayoutConstraint.activate([ gridContainer.topAnchor.constraint(equalTo: self.topAnchor), gridContainer.bottomAnchor.constraint(equalTo: self.bottomAnchor), gridContainer.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: outerPadding), gridContainer.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -outerPadding), ]) let topRow = NSView() topRow.translatesAutoresizingMaskIntoConstraints = false gridContainer.addSubview(topRow) let bottomRow = NSView() bottomRow.translatesAutoresizingMaskIntoConstraints = false gridContainer.addSubview(bottomRow) NSLayoutConstraint.activate([ topRow.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor), topRow.trailingAnchor.constraint(equalTo: gridContainer.trailingAnchor), topRow.topAnchor.constraint(equalTo: gridContainer.topAnchor), topRow.heightAnchor.constraint(equalToConstant: self.rowHeight), bottomRow.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor), bottomRow.trailingAnchor.constraint(equalTo: gridContainer.trailingAnchor), bottomRow.bottomAnchor.constraint(equalTo: gridContainer.bottomAnchor), bottomRow.heightAnchor.constraint(equalToConstant: self.rowHeight), bottomRow.topAnchor.constraint(equalTo: topRow.bottomAnchor, constant: self.rowSpacing), ]) for index in 0..<columns { let xOffset = CGFloat(index) * (uniformWidth + computedGap) if index < topButtons.count { let button = topButtons[index] NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor, constant: xOffset), button.centerYAnchor.constraint(equalTo: topRow.centerYAnchor), ]) } if index < bottomButtons.count { let button = bottomButtons[index] NSLayoutConstraint.activate([ button.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor, constant: xOffset), button.centerYAnchor.constraint(equalTo: bottomRow.centerYAnchor), ]) } } } private static func shouldUseTwoRows( count: Int, maxAllowedSegmentWidth: CGFloat, stackedIcons: Bool) -> Bool { guard count > 1 else { return false } let minimumComfortableAverage: CGFloat = stackedIcons ? 44 : 54 return maxAllowedSegmentWidth < minimumComfortableAverage } private static func switcherOuterPadding(for width: CGFloat, count: Int, minimumGap: CGFloat) -> CGFloat { // Align with the card's left/right content grid when possible. let preferred: CGFloat = 16 let reduced: CGFloat = 10 let minimal: CGFloat = 6 func averageButtonWidth(outerPadding: CGFloat) -> CGFloat { let available = width - outerPadding * 2 - minimumGap * CGFloat(max(0, count - 1)) guard count > 0 else { return 0 } return available / CGFloat(count) } // Only sacrifice padding when we'd otherwise squeeze buttons into unreadable widths. let minimumComfortableAverage: CGFloat = count >= 5 ? 46 : 54 if averageButtonWidth(outerPadding: preferred) >= minimumComfortableAverage { return preferred } if averageButtonWidth(outerPadding: reduced) >= minimumComfortableAverage { return reduced } return minimal } @available(*, unavailable) required init?(coder: NSCoder) { nil } override var intrinsicContentSize: NSSize { NSSize(width: self.preferredWidth, height: self.frame.size.height) } @objc private func handleSelection(_ sender: NSButton) { let index = sender.tag guard self.segments.indices.contains(index) else { return } for (idx, button) in self.buttons.enumerated() { button.state = (idx == index) ? .on : .off } self.updateButtonStyles() self.onSelect(self.segments[index].provider) } private func updateButtonStyles() { for button in self.buttons { let isSelected = button.state == .on let isHovered = self.hoveredButtonTag == button.tag button.contentTintColor = isSelected ? self.selectedTextColor : self.unselectedTextColor button.layer?.backgroundColor = if isSelected { self.selectedBackground } else if isHovered { self.hoverPlateColor() } else { self.unselectedBackground } self.updateWeeklyIndicatorVisibility(for: button) (button as? StackedToggleButton)?.setContentTintColor(button.contentTintColor) (button as? InlineIconToggleButton)?.setContentTintColor(button.contentTintColor) } } private func isLightMode() -> Bool { self.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .aqua } private func updateLightModeStyling() { guard self.isLightMode() else { self.lightModeOverlayLayer.backgroundColor = nil return } // The menu card background is very bright in light mode; add a subtle neutral wash to ground the switcher. self.lightModeOverlayLayer.backgroundColor = NSColor.black.withAlphaComponent(0.035).cgColor } private func hoverPlateColor() -> CGColor { if self.isLightMode() { return NSColor.black.withAlphaComponent(0.095).cgColor } return NSColor.labelColor.withAlphaComponent(0.06).cgColor } // Cache for button width measurements to avoid repeated layout passes. private static var buttonWidthCache: [ObjectIdentifier: CGFloat] = [:] private static func maxToggleWidth(for button: NSButton) -> CGFloat { let buttonId = ObjectIdentifier(button) // Return cached value if available. if let cached = buttonWidthCache[buttonId] { return cached } let originalState = button.state