1 // Copyright (C) 2024 Rubén Beltrán del Río
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.
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.
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.
17 struct MapEditor: View {
18 @Binding var document: MapDocument
20 @State var selectedEvolution: StageType = .behavior
21 @State var isSearching: Bool = false
23 @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
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
31 var results: [Range<String.Index>] {
32 if !isSearching || searchTerm.isEmpty {
35 let options: NSString.CompareOptions = [.caseInsensitive, .diacriticInsensitive]
36 var searchRange = document.text.startIndex..<document.text.endIndex
37 var ranges: [Range<String.Index>] = []
39 while let range = document.text.range(of: searchTerm, options: options, range: searchRange) {
41 searchRange = range.upperBound..<document.text.endIndex
55 if results.count > 0 {
56 selectedTerm = (selectedTerm + 1) % results.count
62 if results.count > 0 {
63 if selectedTerm == 0 {
64 selectedTerm = results.count - 1
66 selectedTerm = (selectedTerm - 1) % results.count
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(
95 GeometryReader { geometry in
96 ScrollView([.horizontal, .vertical]) {
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)
104 MagnificationGesture()
105 .onChanged { value in
106 let delta = value / lastZoom
108 zoom = min(max(zoom * delta, zoomRange.lowerBound), zoomRange.upperBound)
120 value: $zoom, in: zoomRange, step: 0.1,
122 Text(formatZoom(zoom))
123 .font(.Theme.smallControl)
126 Image(systemName: "minus.magnifyingglass")
127 .font(.Theme.smallControl)
128 .help("Zoom Out (⌘-)")
131 Image(systemName: "plus.magnifyingglass")
132 .font(.Theme.smallControl)
133 .help("Zoom In (⌘+)")
135 ).frame(width: 200).padding(.trailing, 10.0)
139 EvolutionPicker(selectedEvolution: $selectedEvolution)
141 }.focusedSceneValue(\.isSearching, $isSearching)
142 .focusedSceneValue(\.selectedEvolution, $selectedEvolution)
146 func adaptiveStack<Content: View>(@ViewBuilder content: () -> Content) -> some View {
147 if viewStyle == .horizontal {
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"
166 private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
171 MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!)