]> git.r.bdr.sh - rbdr/map/blob - Map/Presentation/MapEditor.swift
8c5ab2214c3cc7283d9d95044753222a205a2445
[rbdr/map] / Map / Presentation / MapEditor.swift
1 /*
2 Copyright (C) 2024 Rubén Beltrán del Río
3
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see https://map.tranquil.systems.
16 */
17 import SwiftUI
18
19 struct MapEditor: View {
20 @Binding var document: MapDocument
21 var url: URL?
22 @State var selectedEvolution: StageType = .behavior
23
24 @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
25
26 let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
27 @AppStorage("zoom") var zoom = 1.0
28 @State var lastZoom = 1.0
29
30 var body: some View {
31 VStack(spacing: 0) {
32 adaptiveStack {
33 ZStack(alignment: .topLeading) {
34 MapTextEditor(document: $document)
35 .background(Color.ui.background)
36 .foregroundColor(Color.ui.foreground)
37 .frame(minHeight: 96.0)
38 }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.ui.background).cornerRadius(
39 5.0)
40 GeometryReader { geometry in
41 ScrollView([.horizontal, .vertical]) {
42 MapRenderView(
43 document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex
44 ).scaleEffect(zoom, anchor: .center).frame(
45 width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom,
46 height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom)
47 }.background(Color.ui.background)
48 .gesture(
49 MagnificationGesture()
50 .onChanged { value in
51 let delta = value / lastZoom
52 lastZoom = value
53 zoom = min(max(zoom * delta, zoomRange.lowerBound), zoomRange.upperBound)
54 }
55 .onEnded { _ in
56 lastZoom = 1.0
57 }
58 )
59 }
60 }
61 Divider()
62 HStack {
63 Spacer()
64 Slider(
65 value: $zoom, in: zoomRange, step: 0.1,
66 label: {
67 Text(formatZoom(zoom))
68 .font(.theme.smallControl)
69 },
70 minimumValueLabel: {
71 Image(systemName: "minus.magnifyingglass")
72 .font(.theme.smallControl)
73 .help("Zoom Out (⌘-)")
74 },
75 maximumValueLabel: {
76 Image(systemName: "plus.magnifyingglass")
77 .font(.theme.smallControl)
78 .help("Zoom In (⌘+)")
79 }
80 ).frame(width: 200).padding(.trailing, 10.0)
81 }.padding(4.0)
82 }.toolbar {
83 HStack {
84 Button(action: saveImage) {
85 Image(systemName: "photo")
86 }
87 .help("Export Image (⌘E)")
88 .padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
89 }
90 EvolutionPicker(selectedEvolution: $selectedEvolution)
91 }
92 }
93
94 @ViewBuilder
95 func adaptiveStack<Content: View>(@ViewBuilder content: () -> Content) -> some View {
96 if viewStyle == .horizontal {
97 VSplitView {
98 content()
99 }
100 } else {
101 HSplitView {
102 content()
103 }
104 }
105 }
106
107 private func formatZoom(_ number: CGFloat) -> String {
108 let formatter = NumberFormatter()
109 formatter.numberStyle = .decimal
110 formatter.maximumFractionDigits = 1
111 formatter.minimumFractionDigits = 1
112 return (formatter.string(from: NSNumber(value: number)) ?? "") + "x"
113 }
114
115 private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
116 }
117
118 private func saveImage() {
119 if let image = document.exportAsImage(withEvolution: selectedEvolution) {
120
121 let filename = url?.deletingPathExtension().lastPathComponent ?? "Untitled"
122
123 let savePanel = NSSavePanel()
124 savePanel.allowedContentTypes = [.png]
125 savePanel.canCreateDirectories = true
126 savePanel.isExtensionHidden = false
127 savePanel.title = "Save \(filename) as image"
128 savePanel.message = "Choose a location to save the image"
129 savePanel.nameFieldStringValue = "\(filename).png"
130 savePanel.begin { result in
131 if result == .OK, let url = savePanel.url {
132 if let tiffRepresentation = image.tiffRepresentation {
133 let bitmapImage = NSBitmapImageRep(data: tiffRepresentation)
134 let pngData = bitmapImage?.representation(using: .png, properties: [:])
135 do {
136 try pngData?.write(to: url)
137 } catch {
138 return
139 }
140 }
141 }
142 }
143 }
144 }
145 }
146
147 #Preview {
148 MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!)
149 }