// GPPanels.swift // SwiftUI panels for the Grease Pencil workspace. import SwiftUI // MARK: - Brush Panel struct GPBrushPanel: View { @State private var selectedBrush: GPBrushType = .draw @State private var brushSize: Float = 25 @State private var brushStrength: Float = 1.0 @State private var brushColor: Color = .black var body: some View { VStack(spacing: 8) { // Brush type selector ForEach(GPBrushType.allCases) { brush in Button { selectedBrush = brush } label: { Image(systemName: brush.icon) .font(.system(size: 16)) .frame(width: 36, height: 36) .background( selectedBrush == brush ? Color.orange.opacity(0.25) : Color.clear, in: RoundedRectangle(cornerRadius: 6) ) } .tint(selectedBrush == brush ? .orange : .secondary) } Divider().padding(.horizontal, 8) // Color ColorPicker("", selection: $brushColor) .labelsHidden() .frame(width: 36, height: 36) Divider().padding(.horizontal, 8) // Quick presets VStack(spacing: 4) { Text("Size") .font(.system(size: 8)) .foregroundStyle(.secondary) Text("\(Int(brushSize))") .font(.system(size: 10, design: .monospaced)) .foregroundStyle(.primary) } Spacer() } .padding(.vertical, 8) .padding(.horizontal, 4) .background(.ultraThinMaterial) } } enum GPBrushType: String, CaseIterable, Identifiable { case draw = "Draw" case fill = "Fill" case erase = "Erase" case tint = "Tint" case smooth = "Smooth" case grab = "Grab" var id: String { rawValue } var icon: String { switch self { case .draw: "pencil.tip" case .fill: "paintbrush.fill" case .erase: "eraser" case .tint: "eyedropper" case .smooth: "wand.and.stars" case .grab: "hand.point.up.left" } } } // MARK: - Layer Panel struct GPLayerPanel: View { @State private var layers: [GPLayerInfo] = [ .init(name: "Fills", isVisible: true, isActive: false), .init(name: "Lines", isVisible: true, isActive: true), .init(name: "Sketch", isVisible: false, isActive: false), ] var body: some View { VStack(alignment: .leading, spacing: 0) { // Header HStack { Text("Layers") .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) Spacer() Button { let new = GPLayerInfo( name: "Layer \(layers.count + 1)", isVisible: true, isActive: false ) layers.insert(new, at: 0) } label: { Image(systemName: "plus") .font(.caption2) } .tint(.secondary) } .padding(.horizontal, 10) .padding(.vertical, 8) Divider() // Layer list ScrollView { LazyVStack(spacing: 0) { ForEach(layers) { layer in GPLayerRow(layer: layer) { // Select for i in layers.indices { layers[i].isActive = (layers[i].id == layer.id) } } } } } Spacer() } .background(.ultraThinMaterial) } } struct GPLayerInfo: Identifiable { let id = UUID() var name: String var isVisible: Bool var isActive: Bool } struct GPLayerRow: View { let layer: GPLayerInfo let onSelect: () -> Void var body: some View { Button(action: onSelect) { HStack(spacing: 8) { // Color swatch RoundedRectangle(cornerRadius: 2) .fill(layer.isActive ? Color.orange : Color.gray.opacity(0.3)) .frame(width: 4, height: 24) Text(layer.name) .font(.caption) .foregroundStyle(layer.isActive ? .primary : .secondary) Spacer() Image(systemName: layer.isVisible ? "eye" : "eye.slash") .font(.system(size: 10)) .foregroundStyle(.secondary) } .padding(.horizontal, 10) .padding(.vertical, 6) .background(layer.isActive ? Color.orange.opacity(0.08) : .clear) } .buttonStyle(.plain) } } // MARK: - Onion Skin Panel struct OnionSkinPanel: View { @State private var enabled = true @State private var framesBefore: Double = 3 @State private var framesAfter: Double = 1 @State private var opacity: Double = 0.5 @State private var colorBefore: Color = .red.opacity(0.3) @State private var colorAfter: Color = .green.opacity(0.3) var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { Text("Onion Skin") .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) Spacer() Toggle("", isOn: $enabled) .labelsHidden() .scaleEffect(0.7) } .padding(.horizontal, 10) .padding(.top, 8) if enabled { VStack(alignment: .leading, spacing: 10) { SliderRow(label: "Before", value: $framesBefore, range: 0...10) SliderRow(label: "After", value: $framesAfter, range: 0...5) SliderRow(label: "Opacity", value: $opacity, range: 0...1) HStack(spacing: 12) { ColorPicker("Before", selection: $colorBefore) .font(.caption2) ColorPicker("After", selection: $colorAfter) .font(.caption2) } } .padding(.horizontal, 10) } Spacer() } .background(.ultraThinMaterial) } } struct SliderRow: View { let label: String @Binding var value: Double let range: ClosedRange var body: some View { VStack(alignment: .leading, spacing: 2) { HStack { Text(label) .font(.system(size: 10)) .foregroundStyle(.secondary) Spacer() Text(String(format: range.upperBound > 1 ? "%.0f" : "%.2f", value)) .font(.system(size: 10, design: .monospaced)) } Slider(value: $value, in: range) .tint(.orange) } } }