Ratul SarnaFormat Swift sources14662849 months agoHistory
863 lines · 33.7 KB · Swift
RawDownload
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 rowCount: Int    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.rowCount = Self.switcherRowCount(            width: width,            count: self.segments.count,            maxAllowedSegmentWidth: initialMaxAllowedSegmentWidth,            stackedIcons: self.stackedIcons)        self.rowSpacing = self.stackedIcons ? 4 : 2        if self.stackedIcons && self.rowCount >= 3 {            self.rowHeight = 40        } else {            self.rowHeight = self.stackedIcons ? 36 : 30        }        let height: CGFloat = self.rowHeight * CGFloat(self.rowCount)            + self.rowSpacing * CGFloat(max(0, self.rowCount - 1))        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.layoutCount(for: self.segments.count, rows: self.rowCount)        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(_:)))                stacked.setAllowsTwoLineTitle(self.rowCount >= 3)                if self.rowCount >= 4 {                    stacked.setTitleFontSize(NSFont.smallSystemFontSize - 3)                }                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.rowCount > 1 || !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.rowCount > 1 {            self.applyMultiRowLayout(                rowCount: self.rowCount,                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 applyMultiRowLayout(        rowCount: Int,        outerPadding: CGFloat,        minimumGap: CGFloat,        uniformWidth: CGFloat)    {        let rows = Self.splitRows(for: self.buttons, rowCount: rowCount)        let columns = rows.map(\.count).max() ?? 0        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),        ])         var rowViews: [NSView] = []        for _ in 0..<rowCount {            let row = NSView()            row.translatesAutoresizingMaskIntoConstraints = false            gridContainer.addSubview(row)            rowViews.append(row)        }         var rowConstraints: [NSLayoutConstraint] = []        for (index, row) in rowViews.enumerated() {            rowConstraints.append(row.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor))            rowConstraints.append(row.trailingAnchor.constraint(equalTo: gridContainer.trailingAnchor))            rowConstraints.append(row.heightAnchor.constraint(equalToConstant: self.rowHeight))            if index == 0 {                rowConstraints.append(row.topAnchor.constraint(equalTo: gridContainer.topAnchor))            } else {                rowConstraints.append(row.topAnchor.constraint(                    equalTo: rowViews[index - 1].bottomAnchor,                    constant: self.rowSpacing))            }            if index == rowViews.count - 1 {                rowConstraints.append(row.bottomAnchor.constraint(equalTo: gridContainer.bottomAnchor))            }        }        NSLayoutConstraint.activate(rowConstraints)         for (rowIndex, rowButtons) in rows.enumerated() {            guard rowIndex < rowViews.count else { continue }            let rowView = rowViews[rowIndex]            for (columnIndex, button) in rowButtons.enumerated() {                let xOffset = CGFloat(columnIndex) * (uniformWidth + computedGap)                NSLayoutConstraint.activate([                    button.leadingAnchor.constraint(equalTo: gridContainer.leadingAnchor, constant: xOffset),                    button.centerYAnchor.constraint(equalTo: rowView.centerYAnchor),                ])            }        }    }     private static func switcherRowCount(        width: CGFloat,        count: Int,        maxAllowedSegmentWidth: CGFloat,        stackedIcons: Bool) -> Int    {        guard count > 1 else { return 1 }        let maxRows = min(4, count)        let fourRowThreshold = 15        let minimumComfortableAverage: CGFloat = stackedIcons ? 50 : 54        if count >= fourRowThreshold { return maxRows }        if maxAllowedSegmentWidth >= minimumComfortableAverage { return 1 }         for rows in 2...maxRows {            let perRow = self.layoutCount(for: count, rows: rows)            let outerPadding = self.switcherOuterPadding(for: width, count: perRow, minimumGap: 1)            let allowedWidth = self.maxAllowedUniformSegmentWidth(                for: width,                count: perRow,                outerPadding: outerPadding,                minimumGap: 1)            if allowedWidth >= minimumComfortableAverage { return rows }        }         return maxRows    }     private static func layoutCount(for count: Int, rows: Int) -> Int {        guard rows > 0 else { return count }        return Int(ceil(Double(count) / Double(rows)))    }     private static func splitRows(for buttons: [NSButton], rowCount: Int) -> [[NSButton]] {        guard rowCount > 1 else { return [buttons] }        let base = buttons.count / rowCount        let extra = buttons.count % rowCount        var rows: [[NSButton]] = []        var start = 0        for index in 0..<rowCount {            let size = base + (index < extra ? 1 : 0)            if size == 0 {                rows.append([])                continue            }            let end = min(buttons.count, start + size)            rows.append(Array(buttons[start..<end]))            start = end        }        return rows    }     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 ? 50 : 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 {

Lines 1–500 of 863.