gh-EmergeTools-Pow/Sources/Pow/Infrastructure/AngleControl.swift
2024-10-15 18:43:29 +02:00

139 lines
3.7 KiB
Swift

#if !os(tvOS)
import SwiftUI
@available(watchOS 9.0, *)
struct AngleControl<Label: View>: View {
@Binding
var angle: Angle
var label: Label
init(angle: Binding<Angle>, @ViewBuilder label: () -> Label) {
self._angle = angle
self.label = label()
}
@State
private var lastAngle: Angle = .zero
@GestureState
private var dragAngle: Angle = .zero
@Environment(\.controlSize)
private var controlSize
private var dragGesture: some Gesture {
DragGesture(minimumDistance: 0)
.updating($dragAngle) { value, state, _ in
state = .degrees(-value.translation.height * 2)
}
.onChanged { value in
angle = lastAngle + .degrees(-value.translation.height * 2)
}
.onEnded { value in
angle = lastAngle + .degrees(-value.translation.height * 2)
lastAngle = angle
}
}
private var size: CGFloat {
switch controlSize {
case .mini: return 32
case .small: return 38
case .regular: return 44
case .large: return 54
#if compiler(>=5.9)
// ControlSize.extraLarge is only available from Xcode 15 which comes with Swift 5.9
case .extraLarge: return 54
#endif
@unknown default: return 44
}
}
var body: some View {
let content = ZStack {
Circle()
.fill(.gray.opacity(dragAngle == .zero ? 0.1 : 0.2))
.animation(.easeOut(duration: dragAngle == .zero ? 0.3 : 0.05), value: dragAngle == .zero)
Circle()
.stroke(.quaternary)
ZStack(alignment: .leading) {
Color.clear
Capsule(style: .continuous)
.fill(.tint)
.frame(width: size / 4, height: 2)
.padding(4)
}
.rotationEffect(angle)
}
.frame(width: size, height: size)
if #available(iOS 16.0, macOS 13, *) {
LabeledContent {
content
} label: {
label
}
.gesture(dragGesture)
} else {
content
.gesture(dragGesture)
}
}
}
@available(watchOS 9.0, *)
extension AngleControl where Label == Text {
init(_ title: some StringProtocol, angle: Binding<Angle>) {
self._angle = angle
self.label = Text(title)
}
init(_ titleKey: LocalizedStringKey, angle: Binding<Angle>) {
self._angle = angle
self.label = Text(titleKey)
}
init(angle: Binding<Angle>) {
self._angle = angle
let measurement = Measurement<UnitAngle>(value: angle.wrappedValue.degrees, unit: .degrees)
let formatted = measurement
.formatted(
.measurement(
width: .narrow,
numberFormatStyle: .number.precision(.fractionLength(0))
)
)
self.label = Text(formatted)
}
}
@available(watchOS 9.0, *)
struct AngleControl_Previews: PreviewProvider {
struct Preview: View {
@State var angle: Angle = .zero
var body: some View {
VStack {
Rectangle()
.fill(.red)
.frame(width: 100, height: 1)
.rotationEffect(angle)
AngleControl(angle: $angle)
}
.monospacedDigit()
}
}
static var previews: some View {
VStack(spacing: 32) {
ForEach(ControlSize.allCases, id: \.self) { size in
Preview()
.controlSize(size)
}
}
.padding()
}
}
#endif