// Copyright (C) 2024 Rubén Beltrán del Río // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import SwiftUI struct MapEditor: View { @Binding var document: MapDocument var url: URL? @State var selectedEvolution: StageType = .behavior @State var isSearching: Bool = false @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal let zoomRange = Constants.kMinZoom...Constants.kMaxZoom @AppStorage("zoom") var zoom = 1.0 @State var lastZoom = 1.0 @State var searchTerm = "" @State var selectedTerm = 0 var results: [Range] { if !isSearching || searchTerm.isEmpty { return [] } let options: NSString.CompareOptions = [.caseInsensitive, .diacriticInsensitive] var searchRange = document.text.startIndex..] = [] while let range = document.text.range(of: searchTerm, options: options, range: searchRange) { ranges.append(range) searchRange = range.upperBound.. 0 { selectedTerm = (selectedTerm + 1) % results.count } } }, onPrevious: { withAnimation { if results.count > 0 { if selectedTerm == 0 { selectedTerm = results.count - 1 } else { selectedTerm = (selectedTerm - 1) % results.count } } } }, onSubmit: { }, onDismiss: { withAnimation { isSearching = false } } ) .onChange( of: searchTerm, { selectedTerm = 0 }) Divider() } adaptiveStack { ZStack(alignment: .topLeading) { MapTextEditor(document: $document, highlightRanges: results, selectedRange: selectedTerm) .background(Color.UI.background) .foregroundColor(Color.UI.foreground) .frame(minHeight: 96.0) }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.UI.background).cornerRadius( 5.0) GeometryReader { geometry in ScrollView([.horizontal, .vertical]) { MapRenderView( document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex ).scaleEffect(zoom, anchor: .center).frame( width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom, height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom) }.background(Color.UI.background) .gesture( MagnificationGesture() .onChanged { value in let delta = value / lastZoom lastZoom = value zoom = min(max(zoom * delta, zoomRange.lowerBound), zoomRange.upperBound) } .onEnded { _ in lastZoom = 1.0 } ) } } Divider() HStack { Spacer() Slider( value: $zoom, in: zoomRange, step: 0.1, label: { Text(formatZoom(zoom)) .font(.Theme.smallControl) }, minimumValueLabel: { Image(systemName: "minus.magnifyingglass") .font(.Theme.smallControl) .help("Zoom Out (⌘-)") }, maximumValueLabel: { Image(systemName: "plus.magnifyingglass") .font(.Theme.smallControl) .help("Zoom In (⌘+)") } ).frame(width: 200).padding(.trailing, 10.0) }.padding(4.0) }.toolbar { HStack { EvolutionPicker(selectedEvolution: $selectedEvolution) } }.focusedSceneValue(\.isSearching, $isSearching) .focusedSceneValue(\.selectedEvolution, $selectedEvolution) } @ViewBuilder func adaptiveStack(@ViewBuilder content: () -> Content) -> some View { if viewStyle == .horizontal { VSplitView { content() } } else { HSplitView { content() } } } private func formatZoom(_ number: CGFloat) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 1 formatter.minimumFractionDigits = 1 return (formatter.string(from: NSNumber(value: number)) ?? "") + "x" } private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) { } } #Preview { MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!) }