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() .drawingGroup() } 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)) }}
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()
.drawingGroup()
} 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))
}
}