import SwiftUI /// Interactive waveform display with playback progress and seek. struct WaveformView: View { let samples: [WaveformGenerator.WaveformSample] let progress: Double let isLoading: Bool let onSeek: (Double) -> Void @State private var hoveredProgress: Double? @State private var isDragging = false var body: some View { GeometryReader { geometry in ZStack { if isLoading { ProgressView("Generating waveform...") .font(.caption) } else if samples.isEmpty { RoundedRectangle(cornerRadius: 4) .fill(.quaternary) } else { // Waveform canvas Canvas { context, size in drawWaveform(context: context, size: size) } .gesture(waveformGesture(width: geometry.size.width)) .onHover { hovering in if !hovering { hoveredProgress = nil } } .onContinuousHover { phase in switch phase { case .active(let location): hoveredProgress = location.x / geometry.size.width case .ended: hoveredProgress = nil @unknown default: break } } // Hover indicator if let hovered = hoveredProgress, !isDragging { Rectangle() .fill(.white.opacity(0.3)) .frame(width: 1) .position(x: hovered * geometry.size.width, y: geometry.size.height / 2) .allowsHitTesting(false) } } } .clipShape(RoundedRectangle(cornerRadius: 6)) .background( RoundedRectangle(cornerRadius: 6) .fill(.black.opacity(0.3)) ) } } // MARK: - Drawing private func drawWaveform(context: GraphicsContext, size: CGSize) { let width = size.width let height = size.height let midY = height / 2 let count = samples.count guard count > 0 else { return } let playedIndex = Int(progress * Double(count)) // Draw each column for (index, sample) in samples.enumerated() { let x = CGFloat(index) / CGFloat(count) * width let barWidth = max(1, width / CGFloat(count)) let maxHeight = CGFloat(sample.max) * midY let minHeight = CGFloat(sample.min) * midY let rect = CGRect( x: x, y: midY - maxHeight, width: barWidth, height: maxHeight - minHeight ) let color: Color if index < playedIndex { color = .accentColor } else { color = .gray.opacity(0.5) } context.fill(Path(rect), with: .color(color)) } // Playhead line let playheadX = progress * width var playheadPath = Path() playheadPath.move(to: CGPoint(x: playheadX, y: 0)) playheadPath.addLine(to: CGPoint(x: playheadX, y: height)) context.stroke(playheadPath, with: .color(.white), lineWidth: 1.5) } // MARK: - Gesture private func waveformGesture(width: CGFloat) -> some Gesture { DragGesture(minimumDistance: 0) .onChanged { value in isDragging = true let prog = max(0, min(1, value.location.x / width)) onSeek(prog) } .onEnded { _ in isDragging = false } } }