import SwiftUI import SpriteKit import NetworkCityCore /// The SpriteKit city with a glassy stats HUD floated on top. struct CityView: View { @ObservedObject var controller: TrafficController var body: some View { ZStack(alignment: .topLeading) { SpriteView(scene: controller.scene, options: [.ignoresSiblingOrder]) .ignoresSafeArea() hud .padding(14) if let inspection = controller.selection { inspector(inspection) .padding(14) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) .transition(.move(edge: .trailing).combined(with: .opacity)) } } .animation(.easeOut(duration: 0.18), value: controller.selection?.id) .onAppear { controller.start() } } // MARK: - Inspector panel static func color(_ c: TrafficClass) -> Color { switch c { case .dns: return Color(red: 1.00, green: 0.82, blue: 0.25) case .https: return Color(red: 0.22, green: 0.88, blue: 1.00) case .http: return Color(red: 1.00, green: 0.48, blue: 0.24) case .quic: return Color(red: 0.71, green: 0.48, blue: 1.00) case .other: return Color(red: 0.55, green: 0.60, blue: 0.72) } } private func inspector(_ insp: OrgInspection) -> some View { VStack(alignment: .leading, spacing: 10) { HStack { Text(insp.title) .font(.system(size: 16, weight: .bold, design: .monospaced)) .foregroundStyle(.white) Spacer(minLength: 12) Button { controller.deselect() } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.white.opacity(0.5)) } .buttonStyle(.plain) } HStack(spacing: 16) { Text("▼ \(fmt(insp.downKBs))").foregroundStyle(.cyan) Text("▲ \(fmt(insp.upKBs))").foregroundStyle(.orange) Spacer(minLength: 4) Text("\(insp.endpointCount) pts").foregroundStyle(.white.opacity(0.6)) } .font(.system(size: 12, weight: .semibold, design: .monospaced)) if !insp.processes.isEmpty { Text(insp.processes.joined(separator: ", ")) .font(.system(size: 10, design: .monospaced)) .foregroundStyle(.white.opacity(0.55)) .lineLimit(2) } Divider().overlay(.white.opacity(0.15)) Text("ENDPOINTS").font(.system(size: 9, weight: .bold, design: .monospaced)) .foregroundStyle(.white.opacity(0.4)) ScrollView { VStack(alignment: .leading, spacing: 7) { ForEach(insp.endpoints) { ep in endpointRow(ep) } } } .frame(maxHeight: 320) } .padding(14) .frame(width: 290) .background(.black.opacity(0.62), in: RoundedRectangle(cornerRadius: 12)) .overlay(RoundedRectangle(cornerRadius: 12).stroke(.white.opacity(0.1))) } private func endpointRow(_ ep: EndpointInfo) -> some View { HStack(alignment: .top, spacing: 8) { Circle().fill(Self.color(ep.topClass)).frame(width: 7, height: 7) .shadow(color: Self.color(ep.topClass), radius: 3) .padding(.top, 3) VStack(alignment: .leading, spacing: 1) { Text(ep.host) .font(.system(size: 11, design: .monospaced)) .foregroundStyle(.white.opacity(0.9)) if let rdns = ep.rdns { Text(rdns) .font(.system(size: 9, design: .monospaced)) .foregroundStyle(.white.opacity(0.45)) .lineLimit(1).truncationMode(.middle) } if let rtt = ep.rttMs { Text("\(Int(rtt)) ms") .font(.system(size: 9, design: .monospaced)) .foregroundStyle(.white.opacity(0.5)) } } Spacer(minLength: 6) VStack(alignment: .trailing, spacing: 1) { if ep.downKBs >= 0.1 { Text("▼\(fmt(ep.downKBs))").foregroundStyle(.cyan.opacity(0.85)) } if ep.upKBs >= 0.1 { Text("▲\(fmt(ep.upKBs))").foregroundStyle(.orange.opacity(0.85)) } if ep.downKBs < 0.1 && ep.upKBs < 0.1 { Text("idle").foregroundStyle(.white.opacity(0.3)) } } .font(.system(size: 9, weight: .semibold, design: .monospaced)) } } private var hud: some View { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Circle() .fill(controller.live ? Color.green : Color.orange) .frame(width: 8, height: 8) Text("NETWORKCITY") .font(.system(size: 14, weight: .bold, design: .monospaced)) .foregroundStyle(.white.opacity(0.9)) } Divider().frame(width: 168).overlay(.white.opacity(0.15)) stat("▼ down", controller.downKBs, .cyan) stat("▲ up", controller.upKBs, .orange) stat(label: "districts", value: "\(controller.districtCount)") Divider().frame(width: 168).overlay(.white.opacity(0.15)) legend Text(controller.live ? "live · color = protocol · motion = direction" : "warming up…") .font(.system(size: 10, design: .monospaced)) .foregroundStyle(.white.opacity(0.4)) .padding(.top, 2) } .padding(12) .background(.black.opacity(0.45), in: RoundedRectangle(cornerRadius: 10)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(.white.opacity(0.08))) } private static let legendItems: [(String, Color)] = [ ("DNS", Color(red: 1.00, green: 0.82, blue: 0.25)), ("HTTPS", Color(red: 0.22, green: 0.88, blue: 1.00)), ("HTTP", Color(red: 1.00, green: 0.48, blue: 0.24)), ("QUIC", Color(red: 0.71, green: 0.48, blue: 1.00)), ("Other", Color(red: 0.55, green: 0.60, blue: 0.72)), ] private var legend: some View { VStack(alignment: .leading, spacing: 4) { ForEach(Self.legendItems, id: \.0) { name, color in HStack(spacing: 8) { Circle().fill(color).frame(width: 7, height: 7) .shadow(color: color, radius: 3) Text(name) .font(.system(size: 11, design: .monospaced)) .foregroundStyle(.white.opacity(0.8)) } } } .frame(width: 168, alignment: .leading) } private func stat(_ label: String, _ kbs: Double, _ color: Color) -> some View { HStack { Text(label) .font(.system(size: 12, design: .monospaced)) .foregroundStyle(color.opacity(0.9)) Spacer(minLength: 16) Text(format(kbs)) .font(.system(size: 12, weight: .semibold, design: .monospaced)) .foregroundStyle(.white) } .frame(width: 168) } private func stat(label: String, value: String) -> some View { HStack { Text(label).font(.system(size: 12, design: .monospaced)).foregroundStyle(.white.opacity(0.7)) Spacer(minLength: 16) Text(value).font(.system(size: 12, weight: .semibold, design: .monospaced)).foregroundStyle(.white) } .frame(width: 168) } private func format(_ kbs: Double) -> String { kbs >= 1024 ? String(format: "%.1f MB/s", kbs / 1024) : String(format: "%.0f KB/s", kbs) } /// Compact rate for the dense inspector rows. private func fmt(_ kbs: Double) -> String { kbs >= 1024 ? String(format: "%.1fM", kbs / 1024) : String(format: "%.0fK", kbs) } }