208 lines
6.0 KiB
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)
|
|
}
|