diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index a2471c5..b0fda1c 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -24,17 +24,19 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable { var transform: simd_float4x4 = .init() var clipBounds: CGRect = .zero - var zoomScale: CGFloat = .zero var bounds: CGRect = .zero var uniformsBuffer: MTLBuffer? + @Published var state: State = .initial + @Published var zoomScale: CGFloat = .zero + + let zoomPublisher = PassthroughSubject() + init(size: CGSize, canvasID: NSManagedObjectID) { self.size = size self.canvasID = canvasID } - @Published var state: State = .initial - var hasValidStroke: Bool { if let currentStroke = graphicContext.currentStroke { return Date.now.timeIntervalSince(currentStroke.createdAt) * 1000 > 80 @@ -92,7 +94,10 @@ extension Canvas { // MARK: - Zoom Scale extension Canvas { func setZoomScale(_ zoomScale: CGFloat) { - self.zoomScale = zoomScale + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.zoomScale = min(max(zoomScale, minimumZoomScale), maximumZoomScale) + } } } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 50cc89a..1f255d7 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -162,6 +162,12 @@ extension CanvasViewController { } .store(in: &cancellables) + canvas.zoomPublisher + .sink { [weak self] zoomScale in + self?.zoomChanged(zoomScale) + } + .store(in: &cancellables) + tool.$selectedPen .sink { [weak self] pen in self?.penChanged(to: pen) @@ -298,6 +304,12 @@ extension CanvasViewController { } } +extension CanvasViewController { + func zoomChanged(_ zoomScale: CGFloat) { + scrollView.setZoomScale(zoomScale, animated: true) + } +} + extension CanvasViewController { func historyUndid() { guard history.undo() else { return } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index c6a1603..f1700fe 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -17,6 +17,8 @@ struct MemoView: View { @State var title: String @FocusState var textFieldState: Bool + let size: CGFloat = 32 + init(memo: MemoObject) { self.memo = memo self.title = memo.title @@ -30,9 +32,12 @@ struct MemoView: View { .overlay(alignment: .trailing) { PenDock() } + .overlay(alignment: .bottomLeading) { + zoomControl + } .disabled(textFieldState) .overlay(alignment: .top) { - Toolbar(memo: memo) + Toolbar(memo: memo, size: size) } .disabled(canvas.state == .loading || canvas.state == .closing) .overlay { @@ -50,6 +55,40 @@ struct MemoView: View { .environmentObject(history) } + @ViewBuilder + var zoomControl: some View { + let upperBound: CGFloat = 400 + let lowerBound: CGFloat = 10 + let zoomScale: CGFloat = (((canvas.zoomScale - canvas.minimumZoomScale) * (upperBound - lowerBound) / (canvas.maximumZoomScale - canvas.minimumZoomScale)) + lowerBound).rounded() + let zoomScales: [Int] = [400, 200, 100, 75, 50, 25, 10] + Menu { + ForEach(zoomScales, id: \.self) { scale in + Button { + let zoomScale = ((CGFloat(scale) - lowerBound) * (canvas.maximumZoomScale - canvas.minimumZoomScale) / (upperBound - lowerBound)) + canvas.minimumZoomScale + canvas.zoomPublisher.send(zoomScale) + } label: { + Label { + Text(scale, format: .percent) + } icon: { + if CGFloat(scale) == zoomScale { + Image(systemName: "checkmark") + } + } + .font(.headline) + } + } + } label: { + Text(zoomScale / 100, format: .percent) + .frame(width: 45) + .font(.subheadline) + .padding(.horizontal, size / 2.5) + .frame(height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .padding(10) + } + } + func loadingIndicator(_ title: String) -> some View { ProgressView { Text(title) diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 80a82de..ba6500b 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -12,13 +12,17 @@ struct Toolbar: View { @Environment(\.dismiss) var dismiss @EnvironmentObject var history: History - + @EnvironmentObject var canvas: Canvas + @State var memo: MemoObject @State var title: String @FocusState var textFieldState: Bool - init(memo: MemoObject) { + let size: CGFloat + + init(memo: MemoObject, size: CGFloat) { self.memo = memo + self.size = size self.title = memo.title } @@ -27,7 +31,7 @@ struct Toolbar: View { closeButton titleField Spacer() - historyTool + historyControl } .font(.subheadline) .padding(10) @@ -39,7 +43,7 @@ struct Toolbar: View { } label: { Image(systemName: "xmark") .contentShape(.circle) - .padding(10) + .frame(width: size, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) } @@ -51,9 +55,8 @@ struct Toolbar: View { TextField("", text: $title) .focused($textFieldState) .textFieldStyle(.plain) - .padding(.vertical, 5) - .padding(.horizontal, 10) - .frame(width: 120) + .padding(.horizontal, size / 2.5) + .frame(width: 140, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) .onChange(of: textFieldState) { oldValue, newValue in @@ -67,12 +70,13 @@ struct Toolbar: View { } } - var historyTool: some View { + var historyControl: some View { HStack { Button { history.historyPublisher.send(.undo) } label: { Image(systemName: "arrow.uturn.backward.circle") + .contentShape(.circle) } .hoverEffect(.lift) @@ -86,7 +90,7 @@ struct Toolbar: View { .hoverEffect(.lift) .disabled(history.redoDisabled) } - .padding(10) + .frame(width: size * 2, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) .disabled(textFieldState)