+import SwiftUI
+
+struct MapEditor: View {
+ @Binding var document: MapDocument
+ var url: URL?
+ @State var selectedEvolution: StageType = .behavior
+
+ @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
+
+ let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
+ @AppStorage("zoom") var zoom = 1.0
+ @State var lastZoom = 1.0
+
+ var body: some View {
+ VStack(spacing: 0) {
+ adaptiveStack {
+ ZStack(alignment: .topLeading) {
+ MapTextEditor(document: $document)
+ .background(Color.ui.background)
+ .foregroundColor(Color.ui.foreground)
+ .frame(minHeight: 96.0)
+ }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.ui.background).cornerRadius(
+ 5.0)
+ GeometryReader { geometry in
+ ScrollView([.horizontal, .vertical]) {
+ MapRenderView(
+ document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex
+ ).scaleEffect(zoom, anchor: .center).frame(
+ width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom,
+ height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom)
+ }.background(Color.ui.background)
+ .gesture(
+ MagnificationGesture()
+ .onChanged { value in
+ let delta = value / lastZoom
+ lastZoom = value
+ zoom = min(max(zoom * delta, zoomRange.lowerBound), zoomRange.upperBound)
+ }
+ .onEnded { _ in
+ lastZoom = 1.0
+ }
+ )
+ }
+ }
+ Divider()
+ HStack {
+ Spacer()
+ Slider(
+ value: $zoom, in: zoomRange, step: 0.1,
+ label: {
+ Text(formatZoom(zoom))
+ .font(.theme.smallControl)
+ },
+ minimumValueLabel: {
+ Image(systemName: "minus.magnifyingglass")
+ .font(.theme.smallControl)
+ .help("Zoom Out (⌘-)")
+ },
+ maximumValueLabel: {
+ Image(systemName: "plus.magnifyingglass")
+ .font(.theme.smallControl)
+ .help("Zoom In (⌘+)")
+ }
+ ).frame(width: 200).padding(.trailing, 10.0)
+ }.padding(4.0)
+ }.toolbar {
+ HStack {
+ Button(action: saveImage) {
+ Image(systemName: "photo")
+ }
+ .help("Export Image (⌘E)")
+ .padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
+ }
+ EvolutionPicker(selectedEvolution: $selectedEvolution)
+ }
+ }
+
+ @ViewBuilder
+ func adaptiveStack<Content: View>(@ViewBuilder content: () -> Content) -> some View {
+ if viewStyle == .horizontal {
+ VSplitView {
+ content()
+ }
+ } else {
+ HSplitView {
+ content()
+ }
+ }
+ }
+
+ private func formatZoom(_ number: CGFloat) -> String {
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .decimal
+ formatter.maximumFractionDigits = 1
+ formatter.minimumFractionDigits = 1
+ return (formatter.string(from: NSNumber(value: number)) ?? "") + "x"
+ }
+
+ private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
+ print("Dragging: \(vertex), \(x), \(y)")
+ }
+
+ private func saveImage() {
+ if let image = document.exportAsImage(withEvolution: selectedEvolution) {
+
+ let filename = url?.deletingPathExtension().lastPathComponent ?? "Untitled"
+
+ let savePanel = NSSavePanel()
+ savePanel.allowedContentTypes = [.png]
+ savePanel.canCreateDirectories = true
+ savePanel.isExtensionHidden = false
+ savePanel.title = "Save \(filename) as image"
+ savePanel.message = "Choose a location to save the image"
+ savePanel.nameFieldStringValue = "\(filename).png"
+ savePanel.begin { result in
+ if result == .OK, let url = savePanel.url {
+ if let tiffRepresentation = image.tiffRepresentation {
+ let bitmapImage = NSBitmapImageRep(data: tiffRepresentation)
+ let pngData = bitmapImage?.representation(using: .png, properties: [:])
+ do {
+ try pngData?.write(to: url)
+ } catch {
+ return
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+#Preview {
+ MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!)
+}