]> git.r.bdr.sh - rbdr/map/blob - Map/Presentation/MapEditor.swift
Begin re-licensing
[rbdr/map] / Map / Presentation / MapEditor.swift
1 import SwiftUI
2
3 struct MapEditor: View {
4 @Binding var document: MapDocument
5 var url: URL?
6 @State var selectedEvolution: StageType = .behavior
7
8 @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
9
10 let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
11 @AppStorage("zoom") var zoom = 1.0
12 @State var lastZoom = 1.0
13
14 var body: some View {
15 VStack(spacing: 0) {
16 adaptiveStack {
17 ZStack(alignment: .topLeading) {
18 MapTextEditor(document: $document)
19 .background(Color.ui.background)
20 .foregroundColor(Color.ui.foreground)
21 .frame(minHeight: 96.0)
22 }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.ui.background).cornerRadius(
23 5.0)
24 GeometryReader { geometry in
25 ScrollView([.horizontal, .vertical]) {
26 MapRenderView(
27 document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex
28 ).scaleEffect(zoom, anchor: .center).frame(
29 width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom,
30 height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom)
31 }.background(Color.ui.background)
32 .gesture(
33 MagnificationGesture()
34 .onChanged { value in
35 let delta = value / lastZoom
36 lastZoom = value
37 zoom = min(max(zoom * delta, zoomRange.lowerBound), zoomRange.upperBound)
38 }
39 .onEnded { _ in
40 lastZoom = 1.0
41 }
42 )
43 }
44 }
45 Divider()
46 HStack {
47 Spacer()
48 Slider(
49 value: $zoom, in: zoomRange, step: 0.1,
50 label: {
51 Text(formatZoom(zoom))
52 .font(.theme.smallControl)
53 },
54 minimumValueLabel: {
55 Image(systemName: "minus.magnifyingglass")
56 .font(.theme.smallControl)
57 .help("Zoom Out (⌘-)")
58 },
59 maximumValueLabel: {
60 Image(systemName: "plus.magnifyingglass")
61 .font(.theme.smallControl)
62 .help("Zoom In (⌘+)")
63 }
64 ).frame(width: 200).padding(.trailing, 10.0)
65 }.padding(4.0)
66 }.toolbar {
67 HStack {
68 Button(action: saveImage) {
69 Image(systemName: "photo")
70 }
71 .help("Export Image (⌘E)")
72 .padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
73 }
74 EvolutionPicker(selectedEvolution: $selectedEvolution)
75 }
76 }
77
78 @ViewBuilder
79 func adaptiveStack<Content: View>(@ViewBuilder content: () -> Content) -> some View {
80 if viewStyle == .horizontal {
81 VSplitView {
82 content()
83 }
84 } else {
85 HSplitView {
86 content()
87 }
88 }
89 }
90
91 private func formatZoom(_ number: CGFloat) -> String {
92 let formatter = NumberFormatter()
93 formatter.numberStyle = .decimal
94 formatter.maximumFractionDigits = 1
95 formatter.minimumFractionDigits = 1
96 return (formatter.string(from: NSNumber(value: number)) ?? "") + "x"
97 }
98
99 private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
100 print("Dragging: \(vertex), \(x), \(y)")
101 }
102
103 private func saveImage() {
104 if let image = document.exportAsImage(withEvolution: selectedEvolution) {
105
106 let filename = url?.deletingPathExtension().lastPathComponent ?? "Untitled"
107
108 let savePanel = NSSavePanel()
109 savePanel.allowedContentTypes = [.png]
110 savePanel.canCreateDirectories = true
111 savePanel.isExtensionHidden = false
112 savePanel.title = "Save \(filename) as image"
113 savePanel.message = "Choose a location to save the image"
114 savePanel.nameFieldStringValue = "\(filename).png"
115 savePanel.begin { result in
116 if result == .OK, let url = savePanel.url {
117 if let tiffRepresentation = image.tiffRepresentation {
118 let bitmapImage = NSBitmapImageRep(data: tiffRepresentation)
119 let pngData = bitmapImage?.representation(using: .png, properties: [:])
120 do {
121 try pngData?.write(to: url)
122 } catch {
123 return
124 }
125 }
126 }
127 }
128 }
129 }
130 }
131
132 #Preview {
133 MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!)
134 }