#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