diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index f2a4a83..878d439 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -205,6 +205,10 @@ "comment" : "A label displaying the duration of the current song. The argument is the duration of the song in seconds.", "isCommentAutoGenerated" : true }, + "—" : { + "comment" : "A placeholder text used when GPU usage is not available.", + "isCommentAutoGenerated" : true + }, "'Now Playing' was the only option on previous versions and works with all media apps." : { "localizations" : { "ar" : { @@ -6803,6 +6807,10 @@ }, "Enable timer live activity" : { + }, + "Engines" : { + "comment" : "A label describing the GPU engine gauges.", + "isCommentAutoGenerated" : true }, "Enhance your experience with HUDs" : { "localizations" : { diff --git a/DynamicIsland/components/Stats/CircularGaugeView.swift b/DynamicIsland/components/Stats/CircularGaugeView.swift new file mode 100644 index 0000000..758d524 --- /dev/null +++ b/DynamicIsland/components/Stats/CircularGaugeView.swift @@ -0,0 +1,48 @@ +import SwiftUI + +struct CircularGaugeView: View { + let title: String + let value: Double + let tint: Color + let centerPrimaryText: String + var centerSecondaryText: String? = nil + var subtitle: String? = nil + var size: CGFloat = 72 + var lineWidth: CGFloat = 8 + var backgroundTint: Color = Color.primary.opacity(0.08) + + var body: some View { + VStack(spacing: 8) { + ZStack { + Circle() + .stroke(backgroundTint, lineWidth: lineWidth) + .frame(width: size, height: size) + if value > 0 { + RingArc(start: 0, end: min(max(value, 0), 1), color: tint, lineWidth: lineWidth) + .frame(width: size, height: size) + } + VStack(spacing: 2) { + Text(centerPrimaryText) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .foregroundColor(tint) + if let centerSecondaryText, !centerSecondaryText.isEmpty { + Text(centerSecondaryText) + .font(.system(size: 11, weight: .medium)) + .foregroundColor(.secondary) + } + } + } + VStack(spacing: 2) { + Text(title) + .font(.caption) + .foregroundColor(.secondary) + if let subtitle, !subtitle.isEmpty { + Text(subtitle) + .font(.footnote) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + } + } + } +} diff --git a/DynamicIsland/components/Stats/GPUStatsDetailView.swift b/DynamicIsland/components/Stats/GPUStatsDetailView.swift index 62d993b..bbce064 100644 --- a/DynamicIsland/components/Stats/GPUStatsDetailView.swift +++ b/DynamicIsland/components/Stats/GPUStatsDetailView.swift @@ -31,15 +31,15 @@ struct GPUStatsDetailView: View { ) } + StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) { + CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit) + } + if !statsManager.gpuDevices.isEmpty { StatsCard(title: "Devices", padding: 16, background: cardBackground, cornerRadius: 12) { GPUDeviceList(devices: statsManager.gpuDevices, accentColor: accentColor) } } - - StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) { - CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit) - } } .padding(16) } @@ -65,49 +65,15 @@ private struct GPUUsageDashboard: View { let primaryDevice: GPUDeviceMetrics? var body: some View { - HStack(alignment: .center, spacing: 24) { - ZStack { - Circle() - .stroke(accentColor.opacity(0.25), lineWidth: 14) - .frame(width: 120, height: 120) - - RingArc(start: 0, end: CGFloat(min(max(usage / 100, 0), 1)), color: accentColor, lineWidth: 14) - .frame(width: 120, height: 120) - - VStack(spacing: 4) { - Text(StatsFormatting.percentage(usage)) - .font(.system(size: 28, weight: .bold, design: .rounded)) - Text("Active") - .font(.footnote) - .foregroundColor(.secondary) - } + ViewThatFits(in: .horizontal) { + HStack(alignment: .top, spacing: 20) { + leftColumn + breakdownAndMeta } - - VStack(alignment: .leading, spacing: 12) { - DetailRow(color: accentColor.opacity(0.9), label: "Render", value: StatsFormatting.percentage(breakdown.render)) - DetailRow(color: accentColor.opacity(0.7), label: "Compute", value: StatsFormatting.percentage(breakdown.compute)) - DetailRow(color: accentColor.opacity(0.55), label: "Video", value: StatsFormatting.percentage(breakdown.video)) - DetailRow(color: accentColor.opacity(0.45), label: "Other", value: StatsFormatting.percentage(breakdown.other)) + VStack(alignment: .leading, spacing: 20) { + leftColumn + breakdownAndMeta } - .frame(maxWidth: .infinity, alignment: .leading) - - VStack(alignment: .leading, spacing: 8) { - Text("Averages") - .font(.caption) - .foregroundColor(.secondary) - DetailRow(color: nil, label: "Session Avg", value: StatsFormatting.percentage(averageUsage)) - DetailRow(color: nil, label: "Last Update", value: formattedTimestamp(lastUpdated)) - if let device = primaryDevice { - Divider().padding(.vertical, 4) - DetailRow(color: nil, label: "Active GPU", value: device.formattedVendorModel) - DetailRow(color: nil, label: "Status", value: device.isActive ? "Active" : "Idle") - DetailRow(color: nil, label: "Temperature", value: device.temperatureText) - if let cores = device.cores { - DetailRow(color: nil, label: "Cores", value: "\(cores)") - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) } } @@ -117,6 +83,143 @@ private struct GPUUsageDashboard: View { formatter.timeStyle = .medium return formatter.string(from: date) } + + private var leftColumn: some View { + VStack(alignment: .center, spacing: 16) { + usageRing + GPUEngineGauges(render: renderEngineUtilization, tiler: tilerUtilization, accentColor: accentColor) + } + } + + private var breakdownAndMeta: some View { + ViewThatFits(in: .horizontal) { + HStack(alignment: .top, spacing: 20) { + breakdownSection + metaSection + } + VStack(alignment: .leading, spacing: 16) { + breakdownSection + Divider().padding(.vertical, 4) + metaSection + } + } + } + + private var usageRing: some View { + ZStack { + Circle() + .stroke(accentColor.opacity(0.25), lineWidth: 12) + .frame(width: 108, height: 108) + + RingArc(start: 0, end: CGFloat(min(max(usage / 100, 0), 1)), color: accentColor, lineWidth: 12) + .frame(width: 108, height: 108) + + VStack(spacing: 4) { + Text(StatsFormatting.percentage(usage)) + .font(.system(size: 28, weight: .bold, design: .rounded)) + Text("Active") + .font(.footnote) + .foregroundColor(.secondary) + } + } + } + + private var breakdownSection: some View { + VStack(alignment: .leading, spacing: 12) { + DetailRow(color: accentColor.opacity(0.9), label: "Render", value: StatsFormatting.percentage(breakdown.render)) + DetailRow(color: accentColor.opacity(0.7), label: "Compute", value: StatsFormatting.percentage(breakdown.compute)) + DetailRow(color: accentColor.opacity(0.55), label: "Video", value: StatsFormatting.percentage(breakdown.video)) + DetailRow(color: accentColor.opacity(0.45), label: "Other", value: StatsFormatting.percentage(breakdown.other)) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var metaSection: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Averages") + .font(.caption) + .foregroundColor(.secondary) + DetailRow(color: nil, label: "Session Avg", value: StatsFormatting.percentage(averageUsage)) + DetailRow(color: nil, label: "Last Update", value: formattedTimestamp(lastUpdated)) + if let device = primaryDevice { + Divider().padding(.vertical, 4) + DetailRow(color: nil, label: "Active GPU", value: device.formattedVendorModel) + DetailRow(color: nil, label: "Status", value: device.isActive ? "Active" : "Idle") + DetailRow(color: nil, label: "Temperature", value: device.temperatureText) + if let cores = device.cores { + DetailRow(color: nil, label: "Cores", value: "\(cores)") + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var renderEngineUtilization: Double? { + primaryDevice?.renderUtilization ?? breakdown.render + } + + private var tilerUtilization: Double? { + primaryDevice?.tilerUtilization + } +} + +private struct GPUEngineGauges: View { + let render: Double? + let tiler: Double? + let accentColor: Color + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("Engines") + .font(.caption) + .foregroundColor(.secondary) + HStack(alignment: .center, spacing: 16) { + EngineGaugeView(title: "Render", value: render, tint: accentColor) + EngineGaugeView(title: "Tiler", value: tiler, tint: accentColor.opacity(0.75)) + } + } + } +} + +private struct EngineGaugeView: View { + let title: String + let value: Double? + let tint: Color + private let size: CGFloat = 68 + + var body: some View { + if let value { + CircularGaugeView( + title: title, + value: min(max(value / 100, 0), 1), + tint: tint, + centerPrimaryText: StatsFormatting.percentage(value), + centerSecondaryText: nil, + subtitle: nil, + size: size, + lineWidth: 7, + backgroundTint: tint.opacity(0.18) + ) + } else { + placeholder + } + } + + private var placeholder: some View { + VStack(spacing: 8) { + ZStack { + Circle() + .stroke(Color.primary.opacity(0.08), lineWidth: 7) + .frame(width: size, height: size) + Text("—") + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(.secondary) + } + Text(title) + .font(.caption) + .foregroundColor(.secondary) + } + } } private struct GPUDeviceList: View { diff --git a/DynamicIsland/components/Stats/MemoryStatsDetailView.swift b/DynamicIsland/components/Stats/MemoryStatsDetailView.swift index 4c85c01..32df8bd 100644 --- a/DynamicIsland/components/Stats/MemoryStatsDetailView.swift +++ b/DynamicIsland/components/Stats/MemoryStatsDetailView.swift @@ -28,10 +28,6 @@ struct MemoryStatsDetailView: View { MemoryHealthView(breakdown: statsManager.memoryBreakdown, accentColor: accentColor) } - StatsCard(title: "Details", padding: 16, background: cardBackground, cornerRadius: 12) { - MemoryDetailsGrid(breakdown: statsManager.memoryBreakdown, accentColor: accentColor) - } - StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) { CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit) } @@ -78,46 +74,13 @@ private struct MemoryUsageDashboard: View { return ViewThatFits(in: .horizontal) { VStack(alignment: .leading, spacing: 20) { - ZStack { - Circle() - .stroke(freeColor.opacity(0.35), lineWidth: 14) - .frame(width: 128, height: 128) - - MemoryUsageRing(breakdown: breakdown, usedColor: accentColor, freeColor: freeColor) - .frame(width: 128, height: 128) - - VStack(spacing: 4) { - Text(StatsFormatting.percentage(breakdown.usedPercentage)) - .font(.system(size: 30, weight: .bold, design: .rounded)) - Text("Used") - .font(.footnote) - .foregroundColor(.secondary) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - + usageRing totalsSection Divider().padding(.vertical, 4) breakdownSection } HStack(alignment: .top, spacing: 28) { - ZStack { - Circle() - .stroke(freeColor.opacity(0.35), lineWidth: 14) - .frame(width: 128, height: 128) - - MemoryUsageRing(breakdown: breakdown, usedColor: accentColor, freeColor: freeColor) - .frame(width: 128, height: 128) - - VStack(spacing: 4) { - Text(StatsFormatting.percentage(breakdown.usedPercentage)) - .font(.system(size: 30, weight: .bold, design: .rounded)) - Text("Used") - .font(.footnote) - .foregroundColor(.secondary) - } - } - + usageRing totalsSection breakdownSection } @@ -127,6 +90,26 @@ private struct MemoryUsageDashboard: View { private func formattedBytes(_ value: UInt64) -> String { StatsFormatting.bytes(value) } + + private var usageRing: some View { + ZStack { + Circle() + .stroke(freeColor.opacity(0.35), lineWidth: 14) + .frame(width: 128, height: 128) + + MemoryUsageRing(breakdown: breakdown, usedColor: accentColor, freeColor: freeColor) + .frame(width: 128, height: 128) + + VStack(spacing: 4) { + Text(StatsFormatting.percentage(breakdown.usedPercentage)) + .font(.system(size: 30, weight: .bold, design: .rounded)) + Text("Used") + .font(.footnote) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } } private struct MemoryUsageRing: View { @@ -159,31 +142,12 @@ private struct MemoryBreakdownRow: View { } } -private struct MemoryDetailsGrid: View { - let breakdown: MemoryBreakdown - let accentColor: Color - - var body: some View { - VStack(spacing: 10) { - DetailRow(color: accentColor.opacity(0.85), label: "App Memory", value: StatsFormatting.bytes(breakdown.appBytes)) - DetailRow(color: accentColor.opacity(0.7), label: "Cached Files", value: StatsFormatting.bytes(breakdown.cacheBytes)) - DetailRow(color: accentColor.opacity(0.6), label: "Active", value: StatsFormatting.bytes(breakdown.activeBytes)) - DetailRow(color: accentColor.opacity(0.45), label: "Inactive", value: StatsFormatting.bytes(breakdown.inactiveBytes)) - DetailRow(color: accentColor.opacity(0.35), label: "Wired", value: StatsFormatting.bytes(breakdown.wiredBytes)) - DetailRow(color: accentColor.opacity(0.25), label: "Compressed", value: StatsFormatting.bytes(breakdown.compressedBytes)) - DetailRow(color: accentColor.opacity(0.18), label: "Free", value: StatsFormatting.bytes(breakdown.freeBytes)) - Divider().padding(.vertical, 4) - DetailRow(color: nil, label: "Total Memory", value: StatsFormatting.bytes(breakdown.totalBytes)) - } - } -} - private struct MemoryHealthView: View { let breakdown: MemoryBreakdown let accentColor: Color var body: some View { - VStack(alignment: .leading, spacing: 14) { + VStack(alignment: .leading, spacing: 16) { MemoryPressureIndicator(pressure: breakdown.pressure) Divider().padding(.vertical, 4) SwapUsageView(swap: breakdown.swap, accentColor: accentColor)