Monitor/Sources/FloatingMonitor/Views/GaugeView.swift

218 lines
7.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
// MARK: - GaugeDisplayStyle
struct StyledGauge: View {
let title: String
let icon: String
let value: Double
let unit: String
let detail: String
let style: GaugeDisplayStyle
let fontSize: Double
var maxValue: Double = 100
var animateGraphic: Bool = true
var animateText: Bool = true
@ObservedObject private var config = Configuration.shared
private var progress: Double {
maxValue > 0 ? min(value / maxValue, 1.0) : 0
}
private var gaugeColor: Color {
if progress < 0.4 { return .green }
if progress < 0.7 { return .yellow }
if progress < 0.85 { return .orange }
return .red
}
/// gaugeColor
private var textColor: Color {
config.highlightMode ? gaugeColor : .primary
}
private var secondaryColor: Color {
config.highlightMode ? gaugeColor : .secondary
}
/// contentTransition
@ViewBuilder
private func animatedNumber(_ text: Text) -> some View {
if animateText {
text.contentTransition(.numericText(value: value))
} else {
text
}
}
private var graphicAnim: Animation? {
animateGraphic ? .easeOut(duration: 0.2) : nil
}
private var textAnim: Animation? {
animateText ? .easeOut(duration: 0.3) : nil
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: fontSize))
.foregroundColor(secondaryColor)
Text(title)
.font(.system(size: fontSize, weight: .medium))
.foregroundColor(secondaryColor)
Spacer()
}
switch style {
case .bar: barView
case .ring: ringView
case .text: textView
case .pie: pieView
}
}
}
// MARK: -
private var barView: some View {
VStack(spacing: 2) {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule()
.fill(Color.white.opacity(0.12))
.frame(height: 8)
Capsule()
.fill(LinearGradient(
colors: [gaugeColor.opacity(0.8), gaugeColor],
startPoint: .leading, endPoint: .trailing))
.frame(width: max(geo.size.width * CGFloat(progress), 4), height: 8)
.animation(graphicAnim, value: value)
}
}
.frame(height: 8)
HStack {
animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
.foregroundColor(textColor))
Spacer()
Text(detail)
.font(.system(size: fontSize - 2))
.foregroundColor(secondaryColor)
.lineLimit(1)
}
.animation(textAnim, value: value)
}
}
// MARK: -
private var ringView: some View {
HStack(spacing: 10) {
ZStack {
Circle()
.stroke(Color.white.opacity(0.12), lineWidth: 5)
.frame(width: 48, height: 48)
Circle()
.trim(from: 0, to: CGFloat(progress))
.stroke(AngularGradient(
colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)],
center: .center),
style: StrokeStyle(lineWidth: 5, lineCap: .round))
.rotationEffect(.degrees(-90))
.frame(width: 48, height: 48)
.animation(graphicAnim, value: value)
animatedNumber(Text(String(format: "%.0f", value))
.font(.system(size: fontSize - 1, weight: .bold, design: .monospaced))
.foregroundColor(textColor))
}
.animation(textAnim, value: value)
VStack(alignment: .leading, spacing: 2) {
animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
.foregroundColor(textColor))
Text(detail)
.font(.system(size: fontSize - 2))
.foregroundColor(secondaryColor)
.lineLimit(1)
}
.animation(textAnim, value: value)
}
}
// MARK: -
private var textView: some View {
HStack {
animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 6, weight: .bold, design: .monospaced))
.foregroundColor(gaugeColor))
.animation(textAnim, value: value)
Spacer()
Text(detail)
.font(.system(size: fontSize - 2))
.foregroundColor(secondaryColor)
.lineLimit(1)
}
}
// MARK: -
private var pieView: some View {
HStack(spacing: 10) {
ZStack {
Circle()
.fill(Color.white.opacity(0.05))
.frame(width: 44, height: 44)
PieSlice(endAngle: .degrees(360 * progress))
.fill(gaugeColor.opacity(0.85))
.frame(width: 42, height: 42)
.clipShape(Circle())
.animation(graphicAnim, value: value)
animatedNumber(Text(String(format: "%.0f", value))
.font(.system(size: fontSize - 2, weight: .bold, design: .monospaced))
.foregroundColor(.white))
}
.animation(textAnim, value: value)
VStack(alignment: .leading, spacing: 2) {
animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
.foregroundColor(textColor))
Text(detail)
.font(.system(size: fontSize - 2))
.foregroundColor(secondaryColor)
.lineLimit(1)
}
.animation(textAnim, value: value)
}
}
}
// MARK: - Shape
struct PieSlice: Shape {
var endAngle: Angle
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2
path.move(to: center)
path.addArc(center: center, radius: radius,
startAngle: .degrees(-90),
endAngle: endAngle - .degrees(90),
clockwise: false)
path.closeSubpath()
return path
}
}