Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7f24c44c9 | |||
| 0555c5715a | |||
| c419437366 |
208
Sources/Bedrock/Views/Settings/SettingsSlider.swift
Normal file
208
Sources/Bedrock/Views/Settings/SettingsSlider.swift
Normal 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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user