/* Copyright (C) 2024 Rubén Beltrán del Río This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see https://map.tranquil.systems. */ 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(@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) { } 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")!) }