1688 lines · 64.4 KB · Swift
RawDownload
import AppKitimport CodexBarCoreimport QuartzCore enum ProviderSwitcherSelection: Hashable {    case overview    case provider(UsageProvider)} final class ProviderSwitcherView: NSView {    private struct Segment {        let selection: ProviderSwitcherSelection        let image: NSImage        let title: String    }     fileprivate struct QuotaIndicator {        let track: NSView        let fill: NSView        var fillWidthConstraint: NSLayoutConstraint        var fillRatio: CGFloat    }     private let segments: [Segment]    private let onSelect: (ProviderSwitcherSelection) -> Void    private let showsIcons: Bool    private let weeklyRemainingProvider: (UsageProvider) -> Double?    private var buttons: [NSButton] = []    private var quotaIndicators: [ObjectIdentifier: QuotaIndicator] = [:]    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 var pressedButtonTag: Int?    private var selectedSegmentIndex: Int?    private let lightModeOverlayLayer = CALayer()    private static let quotaIndicatorHeight: CGFloat = 2    private static let quotaIndicatorBottomInset: CGFloat = 2    private static let quotaIndicatorHorizontalInset: CGFloat = 8     init(        providers: [UsageProvider],        selected: ProviderSwitcherSelection?,        includesOverview: Bool,        width: CGFloat,        showsIcons: Bool,        iconProvider: (UsageProvider) -> NSImage,        weeklyRemainingProvider: @escaping (UsageProvider) -> Double?,        onSelect: @escaping (ProviderSwitcherSelection) -> Void)    {        let minimumGap: CGFloat = 1        var 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(                selection: .provider(provider),                image: icon,                title: fullTitle)        }        if includesOverview {            let overviewIcon = Self.overviewIcon()            overviewIcon.isTemplate = true            overviewIcon.size = NSSize(width: 16, height: 16)            segments.insert(                Segment(                    selection: .overview,                    image: overviewIcon,                    title: L("Overview")),                at: 0)        }        self.segments = segments        self.onSelect = onSelect        self.showsIcons = showsIcons        self.weeklyRemainingProvider = weeklyRemainingProvider        self.stackedIcons = showsIcons && self.segments.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        self.rowHeight = Self.switcherButtonHeight(stackedIcons: self.stackedIcons, rowCount: self.rowCount)        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()         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            }             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.selection) ? .on : .off            button.toolTip = nil            button.translatesAutoresizingMaskIntoConstraints = false            button.heightAnchor.constraint(equalToConstant: self.rowHeight).isActive = true            self.buttons.append(button)            return button        }         for (index, segment) in self.segments.enumerated() {            let button = makeButton(index: index, segment: segment)            self.addSubview(button)            self.addQuotaIndicator(                to: button,                selection: segment.selection,                remainingPercent: self.remainingPercent(for: segment.selection))        }        self.selectedSegmentIndex = selected.flatMap { selected in            self.segments.firstIndex { $0.selection == selected }        }         let layoutCount = Self.layoutCount(for: self.segments.count, rows: self.rowCount)        let requiredUniformWidth = self.stackedIcons            ? nil            : self.buttons.map(Self.maxToggleWidth(for:)).max()        let layoutMetrics = Self.switcherLayoutMetrics(            for: width,            count: layoutCount,            minimumGap: minimumGap,            requiredSegmentWidth: requiredUniformWidth)         let uniformWidth: CGFloat        if self.rowCount > 1 || !self.stackedIcons {            uniformWidth = self.applyUniformSegmentWidth(maxAllowedWidth: layoutMetrics.maxAllowedSegmentWidth)            if uniformWidth > 0 {                self.segmentWidths = Array(repeating: uniformWidth, count: self.buttons.count)            }        } else {            self.segmentWidths = self.applyNonUniformSegmentWidths(                totalWidth: width,                outerPadding: layoutMetrics.outerPadding,                minimumGap: minimumGap)            uniformWidth = 0        }         self.applyLayout(            outerPadding: layoutMetrics.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()        if let window = self.window {            window.acceptsMouseMovedEvents = true        } else if self.hoveredButtonTag != nil {            self.hoveredButtonTag = nil            self.updateButtonStyles()        }    }     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.button(at: 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()    }     // MARK: - Click handling     override func acceptsFirstMouse(for event: NSEvent?) -> Bool {        // NSMenu's tracking run loop occasionally drops NSButton target-action dispatch when the        // menu is rebuilt under the cursor (e.g. after switching back from a provider tab to        // Overview). The overrides in this section hit-test the parent view, then drive        // selection from mouseDown/mouseUp here so the click never has to round-trip through        // NSButton's tracking loop. See issue #867.        true    }     override func hitTest(_ point: NSPoint) -> NSView? {        let descendant = super.hitTest(point)        if descendant != nil, descendant !== self {            // Swallow any hit on a child NSButton so its tracking loop never sees the click.            return self        }        return descendant    }     override func mouseDown(with event: NSEvent) {        _ = self.handleMenuTrackingMouseDown(event)    }     override func mouseUp(with event: NSEvent) {        _ = self.handleMenuTrackingMouseUp(event)    }     @discardableResult    func handleMenuTrackingMouseDown(_ event: NSEvent) -> Bool {        guard event.type == .leftMouseDown else { return false }        let location = self.locationInView(for: event)        guard let pressedTag = self.button(at: location)?.tag,              self.segments.indices.contains(pressedTag)        else {            return false        }        self.pressedButtonTag = pressedTag        return true    }     @discardableResult    func handleMenuTrackingMouseUp(_ event: NSEvent) -> Bool {        guard event.type == .leftMouseUp else { return false }        defer { self.pressedButtonTag = nil }        guard let pressedTag = self.pressedButtonTag else { return false }        let location = self.locationInView(for: event)        guard let releasedTag = self.button(at: location)?.tag,              releasedTag == pressedTag        else {            return true        }        // Commit only after the matching release. The controller schedules structural menu        // replacement after this callback returns so AppKit can finish the tracking transaction.        self.applySelection(at: pressedTag)        return true    }     private func locationInView(for event: NSEvent) -> NSPoint {        guard let eventWindow = event.window,              let viewWindow = self.window,              eventWindow !== viewWindow        else {            return self.convert(event.locationInWindow, from: nil)        }        let screenLocation = eventWindow.convertPoint(toScreen: event.locationInWindow)        return self.convert(viewWindow.convertPoint(fromScreen: screenLocation), from: nil)    }     func handleKeyboardSelection(at index: Int) -> Bool {        guard self.segments.indices.contains(index) else { return false }        self.applySelection(at: index)        return true    }     private func applySelection(at index: Int) {        let selection = self.segments[index].selection        guard self.selectedSegmentIndex != index else {            self.updateSelection(selection)            return        }        self.updateSelection(selection)        self.onSelect(selection)    }     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))

Lines 1–500 of 1688.