170 lines · 6.2 KB · Swift
RawDownload
import SwiftUI /// Static progress fill with no implicit animations, used inside the menu card.struct UsageProgressBar: View {    private static let paceStripeCount = 3    private static func paceStripeWidth(for scale: CGFloat) -> CGFloat {        2    }     private static func paceStripeSpan(for scale: CGFloat) -> CGFloat {        let stripeCount = max(1, Self.paceStripeCount)        return Self.paceStripeWidth(for: scale) * CGFloat(stripeCount)    }     let percent: Double    let tint: Color    let accessibilityLabel: String    let pacePercent: Double?    let paceOnTop: Bool    @Environment(\.menuItemHighlighted) private var isHighlighted    @Environment(\.displayScale) private var displayScale     init(        percent: Double,        tint: Color,        accessibilityLabel: String,        pacePercent: Double? = nil,        paceOnTop: Bool = true)    {        self.percent = percent        self.tint = tint        self.accessibilityLabel = accessibilityLabel        self.pacePercent = pacePercent        self.paceOnTop = paceOnTop    }     private var clamped: Double {        min(100, max(0, self.percent))    }     var body: some View {        GeometryReader { proxy in            let scale = max(self.displayScale, 1)            let fillWidth = proxy.size.width * self.clamped / 100            let paceWidth = proxy.size.width * Self.clampedPercent(self.pacePercent) / 100            let tipWidth = max(25, proxy.size.height * 6.5)            let stripeInset = 1 / scale            let tipOffset = paceWidth - tipWidth + (Self.paceStripeSpan(for: scale) / 2) + stripeInset            let showTip = self.pacePercent != nil && tipWidth > 0.5            let needsPunchCompositing = showTip            let bar = ZStack(alignment: .leading) {                Capsule()                    .fill(MenuHighlightStyle.progressTrack(self.isHighlighted))                self.actualBar(width: fillWidth)                if showTip {                    self.paceTip(width: tipWidth)                        .offset(x: tipOffset)                }            }            .clipped()            if self.isHighlighted {                bar                    .compositingGroup()            } else if needsPunchCompositing {                bar                    .compositingGroup()            } else {                bar            }        }        .frame(height: 6)        .accessibilityLabel(self.accessibilityLabel)        .accessibilityValue("\(Int(self.clamped)) percent")    }     private func actualBar(width: CGFloat) -> some View {        Capsule()            .fill(MenuHighlightStyle.progressTint(self.isHighlighted, fallback: self.tint))            .frame(width: width)            .contentShape(Rectangle())            .allowsHitTesting(false)    }     private func paceTip(width: CGFloat) -> some View {        let isDeficit = self.paceOnTop == false        let useDeficitRed = isDeficit && self.isHighlighted == false        return GeometryReader { proxy in            let size = proxy.size            let rect = CGRect(origin: .zero, size: size)            let scale = max(self.displayScale, 1)            let stripes = Self.paceStripePaths(size: size, scale: scale)            let stripeColor: Color = if self.isHighlighted {                .white            } else if useDeficitRed {                .red            } else {                .green            }             ZStack {                Canvas { context, _ in                    context.clip(to: Path(rect))                    context.fill(stripes.punched, with: .color(.white.opacity(0.9)))                }                .blendMode(.destinationOut)                 Canvas { context, _ in                    context.clip(to: Path(rect))                    context.fill(stripes.center, with: .color(stripeColor))                }            }        }        .frame(width: width)        .contentShape(Rectangle())        .allowsHitTesting(false)    }     private static func paceStripePaths(size: CGSize, scale: CGFloat) -> (punched: Path, center: Path) {        let rect = CGRect(origin: .zero, size: size)        let extend = size.height * 2        let stripeTopY: CGFloat = -extend        let stripeBottomY: CGFloat = size.height + extend        let align: (CGFloat) -> CGFloat = { value in            (value * scale).rounded() / scale        }         let stripeWidth = Self.paceStripeWidth(for: scale)        let punchWidth = stripeWidth * 3        let stripeInset = 1 / scale        let stripeAnchorX = align(rect.maxX - stripeInset)        let stripeMinY = align(stripeTopY)        let stripeMaxY = align(stripeBottomY)        let anchorTopX = stripeAnchorX        var punchedStripe = Path()        var centerStripe = Path()        let availableWidth = (anchorTopX - punchWidth) - rect.minX        guard availableWidth >= 0 else { return (punchedStripe, centerStripe) }         let punchRightTopX = align(anchorTopX)        let punchLeftTopX = punchRightTopX - punchWidth        let punchRightBottomX = punchRightTopX        let punchLeftBottomX = punchLeftTopX        punchedStripe.addPath(Path { path in            path.move(to: CGPoint(x: punchLeftTopX, y: stripeMinY))            path.addLine(to: CGPoint(x: punchRightTopX, y: stripeMinY))            path.addLine(to: CGPoint(x: punchRightBottomX, y: stripeMaxY))            path.addLine(to: CGPoint(x: punchLeftBottomX, y: stripeMaxY))            path.closeSubpath()        })         let centerLeftTopX = align(punchLeftTopX + (punchWidth - stripeWidth) / 2)        let centerRightTopX = centerLeftTopX + stripeWidth        let centerRightBottomX = centerRightTopX        let centerLeftBottomX = centerLeftTopX        centerStripe.addPath(Path { path in            path.move(to: CGPoint(x: centerLeftTopX, y: stripeMinY))            path.addLine(to: CGPoint(x: centerRightTopX, y: stripeMinY))            path.addLine(to: CGPoint(x: centerRightBottomX, y: stripeMaxY))            path.addLine(to: CGPoint(x: centerLeftBottomX, y: stripeMaxY))            path.closeSubpath()        })         return (punchedStripe, centerStripe)    }     private static func clampedPercent(_ value: Double?) -> Double {        guard let value else { return 0 }        return min(100, max(0, value))    }}