Zum Inhalt

apple/PhysarumApp/Sources/Editor/EditorView.swift

#if !os(tvOS)
import SwiftUI

/// The parameter editor in the web app's look: a dark surface, a header with the
/// preset name + tagSignal dot, and collapsible mono-labelled sections.
struct EditorView: View {
    @Bindable var state: AppState
    @State private var showMorph = false
    // Format lives in the topbar menu (single source of truth). On macOS the topbar is
    // always visible, so the inline editor picker is dropped; on iOS the editor sheet
    // covers the topbar, so it keeps its own segmented control.
    #if !os(macOS)
    @AppStorage("simFormat") private var format: SimFormat = .auto
    #endif

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: Theme.Space.s3) {
                header
                toolbar
                #if !os(macOS)
                Picker("Format", selection: $format) {
                    Text("Auto").tag(SimFormat.auto)
                    Text("Square").tag(SimFormat.square)
                    Text("Portrait").tag(SimFormat.portrait)
                    Text("Landscape").tag(SimFormat.landscape)
                }
                .pickerStyle(.segmented)
                .help("Simulation canvas format — Auto matches the screen")
                #endif
                PhysSection("Global", expanded: true) { GlobalSection(state: state) }
                PhysSection("Species") { SpeciesSection(state: state) }
                PhysSection("Interaction Matrix") { MatrixSection(state: state) }
                PhysSection("Pheromone") { ScalarSection(state: state, section: .pheromone) }
                PhysSection("Dynamics") { ScalarSection(state: state, section: .dynamics) }
                PhysSection("Effects") { ScalarSection(state: state, section: .effects) }
                PhysSection("Audio Reactive") { AudioReactiveSection(state: state) }
            }
            .padding(Theme.Space.s4)
        }
        .background(Theme.Colors.surface)
        .scrollContentBackground(.hidden)
        .foregroundStyle(Theme.Colors.fgPrimary)
        .tint(Theme.Colors.accent)
        .sheet(isPresented: $showMorph) { MorphSheet(state: state, isPresented: $showMorph) }
    }

    private var toolbar: some View {
        HStack(spacing: Theme.Space.s2) {
            toolButton("die.face.5", "Randomize") { state.randomize() }
                .accessibilityLabel("Randomize")
                .accessibilityHint("New random parameters; locked ones stay")
            toolButton("dial.medium", "Mutate") { state.mutate() }
                .accessibilityLabel("Mutate")
                .accessibilityHint("Slightly tweak parameters; locked ones stay")
            toolButton("arrow.left.and.right", "Morph…") { showMorph = true }
                .accessibilityLabel("Morph to preset")
                .accessibilityHint("Blend smoothly to another preset")
            Spacer()
            toolButton("arrow.uturn.backward", "Undo") { state.undoEdit() }
                .disabled(!state.canUndo)
                .keyboardShortcut("z", modifiers: .command)
                .accessibilityLabel("Undo")
                .accessibilityHint("Revert the last parameter change")
                .accessibilityValue(state.canUndo ? "Available" : "Unavailable")
            toolButton("arrow.uturn.forward", "Redo") { state.redoEdit() }
                .disabled(!state.canRedo)
                .keyboardShortcut("z", modifiers: [.command, .shift])
                .accessibilityLabel("Redo")
                .accessibilityHint("Restore the last undone change")
                .accessibilityValue(state.canRedo ? "Available" : "Unavailable")
        }
    }

    private func toolButton(_ icon: String, _ help: String, _ action: @escaping () -> Void) -> some View {
        Button(action: action) {
            Image(systemName: icon)
                .font(.system(size: 13))
                .foregroundStyle(Theme.Colors.fgSecondary)
                .frame(width: 28, height: 24)
        }
        .buttonStyle(.plain)
        .help(help)
    }

    private var header: some View {
        HStack(spacing: Theme.Space.s2) {
            Circle()
                .fill(Theme.Colors.signal(state.selectedPreset.tagSignal.rawValue))
                .frame(width: 8, height: 8)
            Text(state.selectedPreset.name)
                .font(Theme.Fonts.title)
                .foregroundStyle(Theme.Colors.fgPrimary)
            Spacer()
            Toggle("", isOn: $state.autoCycle)
                .labelsHidden()
                .toggleStyle(.switch)
                .controlSize(.mini)
                .tint(Theme.Colors.accent)
                .help("Auto-cycle presets")
                .accessibilityLabel("Auto-cycle")
                .accessibilityHint("Advance presets every 25 seconds")
                .accessibilityValue(A11y.onOff(state.autoCycle))
            if state.isDirty {
                Button { state.resetAll() } label: {
                    Image(systemName: "arrow.uturn.backward")
                        .font(.system(size: 11))
                        .foregroundStyle(Theme.Colors.fgTertiary)
                }
                .buttonStyle(.plain)
                .help("Reset all to preset")
                .accessibilityLabel("Reset all")
                .accessibilityHint("Discard edits; restore preset defaults")
            }
        }
    }
}
#endif