]> git.r.bdr.sh - rbdr/map/blame - Map/Presentation/MapEditor.swift
Bump build version
[rbdr/map] / Map / Presentation / MapEditor.swift
CommitLineData
be897af3 1// Copyright (C) 2024 Rubén Beltrán del Río
98f09799 2
be897af3
RBR
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.
98f09799 7
be897af3
RBR
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.
98f09799 12
be897af3
RBR
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.
e2c37ac1
RBR
15import SwiftUI
16
17struct MapEditor: View {
18 @Binding var document: MapDocument
19 var url: URL?
20 @State var selectedEvolution: StageType = .behavior
14491563 21 @State var isSearching: Bool = false
e2c37ac1
RBR
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
14491563
RBR
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 }
e2c37ac1
RBR
47
48 var body: some View {
49 VStack(spacing: 0) {
14491563
RBR
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 }
e2c37ac1
RBR
87 adaptiveStack {
88 ZStack(alignment: .topLeading) {
14491563 89 MapTextEditor(document: $document, highlightRanges: results, selectedRange: selectedTerm)
be897af3
RBR
90 .background(Color.UI.background)
91 .foregroundColor(Color.UI.foreground)
e2c37ac1 92 .frame(minHeight: 96.0)
be897af3 93 }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.UI.background).cornerRadius(
e2c37ac1
RBR
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)
be897af3 102 }.background(Color.UI.background)
e2c37ac1
RBR
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))
be897af3 123 .font(.Theme.smallControl)
e2c37ac1
RBR
124 },
125 minimumValueLabel: {
126 Image(systemName: "minus.magnifyingglass")
be897af3 127 .font(.Theme.smallControl)
e2c37ac1
RBR
128 .help("Zoom Out (⌘-)")
129 },
130 maximumValueLabel: {
131 Image(systemName: "plus.magnifyingglass")
be897af3 132 .font(.Theme.smallControl)
e2c37ac1
RBR
133 .help("Zoom In (⌘+)")
134 }
135 ).frame(width: 200).padding(.trailing, 10.0)
136 }.padding(4.0)
137 }.toolbar {
138 HStack {
14491563 139 EvolutionPicker(selectedEvolution: $selectedEvolution)
e2c37ac1 140 }
14491563
RBR
141 }.focusedSceneValue(\.isSearching, $isSearching)
142 .focusedSceneValue(\.selectedEvolution, $selectedEvolution)
e2c37ac1
RBR
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) {
e2c37ac1 167 }
e2c37ac1
RBR
168}
169
170#Preview {
171 MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!)
172}