Compare commits

...

3 Commits

View File

@ -0,0 +1,208 @@
//
// 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
/// - Description underneath the title
/// - Slider with optional icons underneath the description
///
/// ```swift
/// SettingsSlider(
/// title: "Ring Size",
/// subtitle: "Adjusts the size of the light ring",
/// value: $settings.ringSize,
/// in: 20...100,
/// step: 5,
/// format: { "\($0)pt" },
/// leadingIcon: Image(systemName: "circle"),
/// trailingIcon: Image(systemName: "circle").font(.title)
/// )
/// ```
public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where Value.Stride: BinaryFloatingPoint {
/// 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?
/// 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 (default: percentage).
/// - 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 = { "\($0)" },
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
}
public var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
Text(title)
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white)
Spacer()
Text(format(value))
.font(.system(size: Design.BaseFontSize.body, weight: .medium, design: .rounded))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Text(subtitle)
.font(.system(size: Design.BaseFontSize.caption))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
HStack(spacing: Design.Spacing.medium) {
if let leadingIcon = leadingIcon {
leadingIcon
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
Slider(value: $value, in: range, step: step)
.tint(accentColor)
if let trailingIcon = trailingIcon {
trailingIcon
.font(.system(size: Design.BaseFontSize.large))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
}
}
.padding(.vertical, Design.Spacing.xSmall)
}
}
// MARK: - Convenience Initializers
/// Creates a percentage slider (0-100%).
public func SettingsSliderPercentage(
title: String,
subtitle: String,
value: Binding<Float>,
in range: ClosedRange<Float> = 0...1,
step: Float.Stride = 0.05,
accentColor: Color = .Accent.primary,
leadingIcon: Image? = nil,
trailingIcon: Image? = nil
) -> SettingsSlider<Float> {
SettingsSlider(
title: title,
subtitle: subtitle,
value: value,
in: range,
step: step,
format: { "\(Int($0 * 100))%" },
accentColor: accentColor,
leadingIcon: leadingIcon,
trailingIcon: trailingIcon
)
}
/// Creates an integer slider with custom unit.
public func SettingsSliderInteger(
title: String,
subtitle: String,
value: Binding<Int>,
in range: ClosedRange<Int>,
step: Int.Stride = 1,
unit: String,
accentColor: Color = .Accent.primary,
leadingIcon: Image? = nil,
trailingIcon: Image? = nil
) -> SettingsSlider<Float> {
SettingsSlider(
title: title,
subtitle: subtitle,
value: Binding(
get: { Float(value.wrappedValue) },
set: { value.wrappedValue = Int($0) }
),
in: Float(range.lowerBound)...Float(range.upperBound),
step: Float(step),
format: { "\(Int($0))\(unit)" },
accentColor: accentColor,
leadingIcon: leadingIcon,
trailingIcon: trailingIcon
)
}
// MARK: - Preview
#Preview {
VStack(spacing: Design.Spacing.medium) {
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: { "\($0)pt" },
leadingIcon: Image(systemName: "circle"),
trailingIcon: Image(systemName: "circle")
)
SettingsSliderPercentage(
title: "Brightness",
subtitle: "Adjusts the brightness of the ring light",
value: .constant(0.75),
leadingIcon: Image(systemName: "sun.min"),
trailingIcon: Image(systemName: "sun.max.fill")
)
}
.padding()
.background(Color.Surface.overlay)
}