Zum Inhalt

apple/PhysarumApp/Sources/Editor/ParamSlider.swift

#if !os(tvOS)
import SwiftUI

/// One slider for any scalar ParamDescriptor. Tap the value → numeric entry;
/// double-tap the row → reset to the preset default; tap the label (with its ⓘ)
/// → a help popover. macOS also shows the help on hover; VoiceOver reads it as a hint.
struct ParamSlider: View {
    @Bindable var state: AppState
    let descriptor: ParamDescriptor
    /// Friendly label override (e.g. the parameter matrix uses short names); the
    /// help tooltip still comes from `descriptor`. `nil` falls back to `descriptor.label`.
    var displayLabel: String? = nil
    @State private var editingText: String?

    private var value: Double { descriptor.get(state.edited) }

    var body: some View {
        VStack(alignment: .leading, spacing: Theme.Space.s1) {
            HStack {
                labelView
                Spacer()
                if state.isModified(descriptor) {
                    Button { state.pushUndo(); state.reset(descriptor) } label: {
                        Image(systemName: "arrow.uturn.backward").font(.system(size: 11))
                            .foregroundStyle(Theme.Colors.fgTertiary)
                    }
                    .buttonStyle(.plain)
                    .accessibilityLabel("Reset \(descriptor.label) to preset")
                }
                LockButton(state: state, id: descriptor.id)
                    .accessibilityLabel(state.isLocked(descriptor.id) ? "Unlock \(descriptor.label)" : "Lock \(descriptor.label)")
                    .accessibilityHint("Keep randomize and mutate from changing this")
                    .accessibilityValue(A11y.locked(state.isLocked(descriptor.id)))
                valueLabel
            }
            Slider(value: Binding(get: { value }, set: { commit($0) }),
                   in: descriptor.range,
                   onEditingChanged: { editing in if editing { state.pushUndo() } })
                .tint(Theme.Colors.accent)
                .controlSize(.small)
                .accessibilityLabel(descriptor.label)
                .accessibilityValue(formatted)
                .accessibilityHint(descriptor.tooltip)
                .precisionControls(value: Binding(get: { value }, set: { commit($0) }),
                                   range: descriptor.range,
                                   baseStep: descriptor.isInt ? 1 : nil,
                                   onEditStart: { state.pushUndo() })
        }
        .contentShape(Rectangle())
        .onTapGesture(count: 2) { state.pushUndo(); state.reset(descriptor) }
    }

    private var labelView: some View {
        HelpLabel(displayLabel ?? descriptor.label, help: descriptor.tooltip)
    }

    @ViewBuilder private var valueLabel: some View {
        if let t = editingText {
            TextField("", text: Binding(get: { t }, set: { editingText = $0 }))
                .frame(width: 64)
                .font(Theme.Fonts.value)
                .textFieldStyle(.roundedBorder)
                .onSubmit { commitText() }
        } else {
            Text(formatted)
                .font(Theme.Fonts.value)
                .foregroundStyle(Theme.Colors.fgPrimary)
                .onTapGesture { editingText = formatted }
        }
    }

    private var formatted: String {
        descriptor.isInt ? String(Int(value.rounded())) : String(format: "%.3g", value)
    }

    private func commit(_ v: Double) {
        var p = state.edited
        descriptor.set(&p, clamp(v))
        state.edited = p
    }

    private func commitText() {
        if let t = editingText, let v = Double(t) { state.pushUndo(); commit(v) }
        editingText = nil
    }

    private func clamp(_ v: Double) -> Double {
        min(max(v, descriptor.range.lowerBound), descriptor.range.upperBound)
    }
}
#endif