Bedrock/Sources/Bedrock/Views/Settings/SettingsSlider.swift

208 lines
6.0 KiB
Swift

//
// SettingsSlider.swift
// Bedrock
//
// A slider setting with title, description, and value display.
//
import SwiftUI
/// A slider setting with title, description, and value display.
///
/// Use this for numeric settings with a slider control, following the pattern:
/// - Title with current value on the right
/// - Subtitle/description underneath the title
/// - Slider with optional icons underneath
///
/// ## Basic Usage
///
/// ```swift
/// SettingsSlider(
/// title: "Ring Size",
/// subtitle: "Adjusts the size of the light ring",
/// value: $settings.ringSize,
/// in: 20...100,
/// step: 5,
/// format: { "\(Int($0))pt" }
/// )
/// ```
///
/// ## With Icons
///
/// ```swift
/// SettingsSlider(
/// title: "Brightness",
/// subtitle: "Adjusts the brightness",
/// value: $brightness,
/// format: .percentage,
/// leadingIcon: Image(systemName: "sun.min"),
/// trailingIcon: Image(systemName: "sun.max.fill")
/// )
/// ```
public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where Value.Stride: BinaryFloatingPoint {
// MARK: - Properties
/// The main title text.
public let title: String
/// The subtitle/description text.
public let subtitle: String
/// Binding to the slider value.
@Binding public var value: Value
/// The range of the slider.
public let range: ClosedRange<Value>
/// The step increment for the slider.
public let step: Value.Stride
/// A closure that formats the value for display.
public let format: (Value) -> String
/// The accent color for the slider.
public let accentColor: Color
/// Optional leading icon for the slider.
public let leadingIcon: Image?
/// Optional trailing icon for the slider.
public let trailingIcon: Image?
// MARK: - Initializer
/// Creates a settings slider.
/// - Parameters:
/// - title: The main title.
/// - subtitle: The subtitle description.
/// - value: Binding to slider value.
/// - range: The range of values (default: 0...1).
/// - step: The step increment (default: 0.1).
/// - format: Closure to format the value display.
/// - accentColor: The accent color (default: primary accent).
/// - leadingIcon: Optional icon on the left side of slider.
/// - trailingIcon: Optional icon on the right side of slider.
public init(
title: String,
subtitle: String,
value: Binding<Value>,
in range: ClosedRange<Value> = 0...1,
step: Value.Stride = 0.1,
format: @escaping (Value) -> String,
accentColor: Color = .Accent.primary,
leadingIcon: Image? = nil,
trailingIcon: Image? = nil
) {
self.title = title
self.subtitle = subtitle
self._value = value
self.range = range
self.step = step
self.format = format
self.accentColor = accentColor
self.leadingIcon = leadingIcon
self.trailingIcon = trailingIcon
}
// MARK: - Body
public var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
Text(title)
.font(.subheadline.weight(.medium))
.foregroundStyle(.primary)
Spacer()
Text(format(value))
.font(.subheadline.weight(.medium))
.fontDesign(.rounded)
.foregroundStyle(.secondary)
}
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
HStack(spacing: Design.Spacing.medium) {
if let leadingIcon {
leadingIcon
.font(.caption2)
.foregroundStyle(.secondary)
}
Slider(value: $value, in: range, step: step)
.tint(accentColor)
if let trailingIcon {
trailingIcon
.font(.callout)
.foregroundStyle(.secondary)
}
}
}
.padding(.vertical, Design.Spacing.xSmall)
}
}
// MARK: - Format Helpers
/// Common format closures for SettingsSlider.
public enum SliderFormat {
/// Formats value as percentage (0.5 "50%").
public static func percentage<V: BinaryFloatingPoint>(_ value: V) -> String {
"\(Int(Double(value) * 100))%"
}
/// Formats value as integer with unit suffix (40 "40pt").
public static func integer<V: BinaryFloatingPoint>(unit: String) -> (V) -> String {
{ "\(Int($0))\(unit)" }
}
/// Formats value as decimal with specified precision.
public static func decimal<V: BinaryFloatingPoint>(precision: Int, unit: String = "") -> (V) -> String {
{ String(format: "%.\(precision)f\(unit)", Double($0)) }
}
}
// MARK: - Preview
#Preview {
VStack(spacing: Design.Spacing.large) {
SettingsSlider(
title: "Ring Size",
subtitle: "Adjusts the size of the light ring around the camera preview",
value: .constant(40.0),
in: 20...100,
step: 5,
format: SliderFormat.integer(unit: "pt"),
leadingIcon: Image(systemName: "circle"),
trailingIcon: Image(systemName: "circle")
)
SettingsSlider(
title: "Brightness",
subtitle: "Adjusts the brightness of the ring light",
value: .constant(0.75),
step: 0.05,
format: SliderFormat.percentage,
leadingIcon: Image(systemName: "sun.min"),
trailingIcon: Image(systemName: "sun.max.fill")
)
SettingsSlider(
title: "Zoom",
subtitle: "Camera zoom level",
value: .constant(1.5),
in: 1.0...5.0,
step: 0.1,
format: SliderFormat.decimal(precision: 1, unit: "x")
)
}
.padding()
.background(Color.Surface.overlay)
}