// // ControlDial.swift // Focale // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // // Horizontal scrub dial for one manual control, with configurable // haptic detents (e.g. a tick every third of a stop — CLAUDE.md §6). // import SwiftUI struct ControlDial: View { let control: CaptureControl let haptics: HapticProfile @Bindable var controls: ManualControls var onChanged: () -> Void @State private var dragStartValue: Double? @State private var lastDetent: Int = -1 var body: some View { VStack(spacing: 4) { Text(controls.displayValue(for: control)) .font(DesignTokens.controlValueFont) .foregroundStyle(.white) .contentTransition(.numericText()) Text(control.displayName) .font(DesignTokens.controlLabelFont) .foregroundStyle(DesignTokens.textSecondary) } .frame(minWidth: 74) .padding(.vertical, 8) .padding(.horizontal, 10) .background(DesignTokens.surfaceRaised, in: RoundedRectangle( cornerRadius: DesignTokens.cornerRadius )) .sensoryFeedback(.selection, trigger: lastDetent) .gesture(dial) .accessibilityLabel(control.displayName) .accessibilityValue(controls.displayValue(for: control)) } private var dial: some Gesture { DragGesture(minimumDistance: 2) .onChanged { gesture in let start = dragStartValue ?? controls.normalizedValue(for: control) if dragStartValue == nil { dragStartValue = start } let sensitivity = 1.0 / 220.0 // full range over ~220 pt let value = (start + gesture.translation.width * sensitivity) .clamped(to: 0...1) controls.setNormalizedValue(value, for: control) if haptics.enabled, haptics.detentCount > 0 { let detent = Int(value * Double(haptics.detentCount)) if detent != lastDetent { lastDetent = detent } } onChanged() } .onEnded { _ in dragStartValue = nil } } }