// // MemoryStatsDetailView.swift // DynamicIsland // // Detailed memory dashboard mirroring the Stats app layout. // // Created by GitHub Copilot on 19/10/2025. // import SwiftUI struct MemoryStatsDetailView: View { @ObservedObject private var statsManager = StatsManager.shared @State private var topProcesses: [ProcessStats] = [] private let accentColor = Color.green private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65) private let processDisplayLimit = 8 var body: some View { ScrollView { VStack(spacing: 16) { StatsCard(title: "Memory Overview", padding: 16, background: cardBackground, cornerRadius: 12) { MemoryUsageDashboard(breakdown: statsManager.memoryBreakdown, accentColor: accentColor) } StatsCard(title: "Pressure & Swap", padding: 16, background: cardBackground, cornerRadius: 12) { 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) } } .padding(16) } .frame(minWidth: 380, minHeight: 420) .onAppear(perform: refreshProcesses) .onReceive(statsManager.$lastUpdated) { _ in refreshProcesses() } } private func refreshProcesses() { let processes = statsManager.getProcessesRankedByMemory() topProcesses = Array(processes.prefix(processDisplayLimit)) } } private struct MemoryUsageDashboard: View { let breakdown: MemoryBreakdown let accentColor: Color private let freeColor = Color.gray.opacity(0.28) var body: some View { HStack(alignment: .center, 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) } } VStack(alignment: .leading, spacing: 12) { DetailRow(color: accentColor.opacity(0.8), label: "Used", value: formattedBytes(breakdown.usedBytes)) DetailRow(color: freeColor.opacity(0.9), label: "Free", value: formattedBytes(breakdown.freeBytes)) DetailRow(color: nil, label: "Total", value: formattedBytes(breakdown.totalBytes)) } .frame(maxWidth: .infinity, alignment: .leading) VStack(alignment: .leading, spacing: 8) { Text("Breakdown") .font(.caption) .foregroundColor(.secondary) MemoryBreakdownRow(label: "App", value: breakdown.appBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.85)) MemoryBreakdownRow(label: "Cached", value: breakdown.cacheBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.65)) MemoryBreakdownRow(label: "Wired", value: breakdown.wiredBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.45)) MemoryBreakdownRow(label: "Compressed", value: breakdown.compressedBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.35)) } .frame(maxWidth: .infinity, alignment: .leading) } } private func formattedBytes(_ value: UInt64) -> String { StatsFormatting.bytes(value) } } private struct MemoryUsageRing: View { let breakdown: MemoryBreakdown let usedColor: Color let freeColor: Color var body: some View { ZStack { let usedFraction = CGFloat(min(max(breakdown.usedPercentage / 100, 0), 1)) if usedFraction > 0 { RingArc(start: 0, end: usedFraction, color: usedColor, lineWidth: 14) } if usedFraction < 1 { RingArc(start: usedFraction, end: 1, color: freeColor.opacity(0.65), lineWidth: 14) } } } } private struct MemoryBreakdownRow: View { let label: String let value: UInt64 let total: UInt64 let color: Color var body: some View { let percent = total > 0 ? Double(value) / Double(total) * 100 : 0 return DetailRow(color: color, label: label, value: "\(StatsFormatting.bytes(value)) ยท \(StatsFormatting.percentage(percent))") } } 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) { MemoryPressureIndicator(pressure: breakdown.pressure) Divider().padding(.vertical, 4) SwapUsageView(swap: breakdown.swap, accentColor: accentColor) } } } private struct MemoryPressureIndicator: View { let pressure: MemoryPressure private var status: (title: String, color: Color, description: String) { switch pressure.level { case .normal: return ("Normal", Color.green, "System memory pressure is nominal.") case .warning: return ("Warning", Color.orange, "Memory pressure is elevated. Closing apps may help.") case .critical: return ("Critical", Color.red, "Memory pressure is critical; the system may purge aggressively.") } } var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Pressure") .font(.caption) .foregroundColor(.secondary) HStack { Capsule() .fill(status.color.opacity(0.16)) .overlay( HStack(spacing: 8) { Circle() .fill(status.color) .frame(width: 10, height: 10) Text(status.title) .font(.system(size: 13, weight: .semibold)) .foregroundColor(status.color) } .padding(.horizontal, 12) ) .frame(height: 28) Spacer() } Text(status.description) .font(.footnote) .foregroundColor(.secondary) } } } private struct SwapUsageView: View { let swap: MemorySwap let accentColor: Color var body: some View { VStack(alignment: .leading, spacing: 10) { Text("Swap") .font(.caption) .foregroundColor(.secondary) if swap.totalBytes > 0 { DetailRow(color: accentColor.opacity(0.85), label: "Used", value: StatsFormatting.bytes(swap.usedBytes)) DetailRow(color: accentColor.opacity(0.55), label: "Free", value: StatsFormatting.bytes(swap.freeBytes)) DetailRow(color: nil, label: "Total", value: StatsFormatting.bytes(swap.totalBytes)) SwapUsageBar(usedBytes: swap.usedBytes, totalBytes: swap.totalBytes, tint: accentColor) .frame(height: 8) } else { Text("Swap is disabled on this system.") .font(.footnote) .foregroundColor(.secondary) } } } } private struct SwapUsageBar: View { let usedBytes: UInt64 let totalBytes: UInt64 let tint: Color var body: some View { GeometryReader { proxy in let width = proxy.size.width let progress = totalBytes > 0 ? min(max(Double(usedBytes) / Double(totalBytes), 0), 1) : 0 ZStack(alignment: .leading) { Capsule() .fill(Color.primary.opacity(0.08)) Capsule() .fill(tint.opacity(0.8)) .frame(width: width * progress) } } } }