import SwiftUI import OnyxDesign /// Der Ladeverlauf als Säulen. /// /// **Säulen und keine Linie.** Eine Linie verbindet auch dort, wo nichts /// gemessen wurde — ein zugeklappter Rechner hat keinen Ladestand gehabt, und /// eine Linie darüber behauptet einen. Säulen können einfach fehlen. public struct BatteryHistoryView: View { private let store: BatteryHistoryStore @State private var window: HistoryWindow = .day public init(store: BatteryHistoryStore) { self.store = store } public var body: some View { VStack(alignment: .leading, spacing: Onyx.Space.sm) { header chart } } private var header: some View { HStack(spacing: Onyx.Space.sm) { if let summary = store.summary(window) { Text(verbatim: "\(percent(summary.low)) – \(percent(summary.high)) %") .font(Onyx.Font.metricSmall) .foregroundStyle(Onyx.Color.textSecondary) } else { Text("battery.history.empty", bundle: .module) .font(Onyx.Font.caption) .foregroundStyle(Onyx.Color.textTertiary) } Spacer(minLength: Onyx.Space.xs) // Drei kurze Wörter statt eines Menüs: die Auswahl hat drei // Möglichkeiten, und ein Menü dafür verlangt zwei Klicks für das, // was ein Klick kann. ForEach(HistoryWindow.allCases, id: \.self) { candidate in Button { window = candidate } label: { Text(candidate.localizedName, bundle: .module) .font(Onyx.Font.micro) .foregroundStyle(candidate == window ? Onyx.Color.accent : Onyx.Color.textTertiary) } .buttonStyle(.plain) } } } private var chart: some View { GeometryReader { geometry in let buckets = store.buckets(window) let gap: CGFloat = 1 let width = max((geometry.size.width - gap * CGFloat(buckets.count - 1)) / CGFloat(buckets.count), 1) HStack(alignment: .bottom, spacing: gap) { ForEach(Array(buckets.enumerated()), id: \.offset) { _, value in if let value { RoundedRectangle(cornerRadius: Onyx.Radius.bar, style: .continuous) .fill(Onyx.Color.accent) .frame(width: width, height: max(geometry.size.height * value, 1)) } else { // Die Lücke bleibt eine Lücke. Color.clear.frame(width: width, height: 1) } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) } .frame(height: 44) } private func percent(_ value: Double) -> Int { Int((min(max(value, 0), 1) * 100).rounded()) } } extension HistoryWindow { var localizedName: LocalizedStringKey { switch self { case .day: "battery.history.day" case .week: "battery.history.week" case .month: "battery.history.month" } } }