]> git.r.bdr.sh - rbdr/map/blob - Map/Presentation/MapEditor.swift
d62d4adfea1bf2259247fa32b7bc874defdbf463
[rbdr/map] / Map / Presentation / MapEditor.swift
1 // Copyright (C) 2024 Rubén Beltrán del Río
2
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
12
13 // You should have received a copy of the GNU General Public License
14 // along with this program. If not, see https://map.tranquil.systems.
15 import SwiftUI
16
17 struct MapEditor: View {
18 @Binding var document: MapDocument
19 var url: URL?
20 @State var selectedEvolution: StageType = .behavior
21 @State var isSearching: Bool = false
22
23 @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
24
25 let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
26 @AppStorage("zoom") var zoom = 1.0
27 @State var lastZoom = 1.0
28 @State var searchTerm = ""
29 @State var selectedTerm = 0
30
31 var results: [Range<String.Index>] {
32 if !isSearching || searchTerm.isEmpty {
33 return []
34 }
35 let options: NSString.CompareOptions = [.caseInsensitive, .diacriticInsensitive]
36 var searchRange = document.text.startIndex..<document.text.endIndex
37 var ranges: [Range<String.Index>] = []
38
39 while let range = document.text.range(of: searchTerm, options: options, range: searchRange) {
40 ranges.append(range)
41 searchRange = range.upperBound..<document.text.endIndex
42 }
43
44 return ranges
45
46 }
47
48 var body: some View {
49 VStack(spacing: 0) {
50 if isSearching {
51 SearchBar(
52 term: $searchTerm,
53 onNext: {
54 withAnimation {
55 if results.count > 0 {
56 selectedTerm = (selectedTerm + 1) % results.count
57 }
58 }
59 },
60 onPrevious: {
61 withAnimation {
62 if results.count > 0 {
63 if selectedTerm == 0 {
64 selectedTerm = results.count - 1
65 } else {
66 selectedTerm = (selectedTerm - 1) % results.count
67 }
68 }
69 }
70 },
71 onSubmit: {
72
73 },
74 onDismiss: {
75 withAnimation {
76 isSearching = false
77 }
78 }
79 )
80 .onChange(
81 of: searchTerm,
82 {
83 selectedTerm = 0
84 })
85 Divider()
86 }
87 adaptiveStack {
88 ZStack(alignment: .topLeading) {
89 MapTextEditor(document: $document, highlightRanges: results, selectedRange: selectedTerm)
90 .background(Color.UI.background)
91 .foregroundColor(Color.UI.foreground)
92 .frame(minHeight: 96.0)
93 }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.UI.background).cornerRadius(
94 5.0)
95 GeometryReader { geometry in
96 ScrollView([.horizontal, .vertical]) {
97 MapRenderView(
98 document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex
99 ).scaleEffect(zoom, anchor: .center).frame(
100 width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom,
101 height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom)
102 }.background(Color.UI.background)
103 .gesture(
104 MagnificationGesture()
105 .onChanged { value in
106 let delta = value / lastZoom
107 lastZoom = value
108 zoom = min(max(zoom * delta, zoomRange.lowerBound), zoomRange.upperBound)
109 }
110 .onEnded { _ in
111 lastZoom = 1.0
112 }
113 )
114 }
115 }
116 Divider()
117 HStack {
118 Spacer()
119 Slider(
120 value: $zoom, in: zoomRange, step: 0.1,
121 label: {
122 Text(formatZoom(zoom))
123 .font(.Theme.smallControl)
124 },
125 minimumValueLabel: {
126 Image(systemName: "minus.magnifyingglass")
127 .font(.Theme.smallControl)
128 .help("Zoom Out (⌘-)")
129 },
130 maximumValueLabel: {
131 Image(systemName: "plus.magnifyingglass")
132 .font(.Theme.smallControl)
133 .help("Zoom In (⌘+)")
134 }
135 ).frame(width: 200).padding(.trailing, 10.0)
136 }.padding(4.0)
137 }.toolbar {
138 HStack {
139 EvolutionPicker(selectedEvolution: $selectedEvolution)
140 }
141 }.focusedSceneValue(\.isSearching, $isSearching)
142 .focusedSceneValue(\.selectedEvolution, $selectedEvolution)
143 }
144
145 @ViewBuilder
146 func adaptiveStack<Content: View>(@ViewBuilder content: () -> Content) -> some View {
147 if viewStyle == .horizontal {
148 VSplitView {
149 content()
150 }
151 } else {
152 HSplitView {
153 content()
154 }
155 }
156 }
157
158 private func formatZoom(_ number: CGFloat) -> String {
159 let formatter = NumberFormatter()
160 formatter.numberStyle = .decimal
161 formatter.maximumFractionDigits = 1
162 formatter.minimumFractionDigits = 1
163 return (formatter.string(from: NSNumber(value: number)) ?? "") + "x"
164 }
165
166 private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
167 }
168 }
169
170 #Preview {
171 MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!)
172 }