diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 6f98d97..258bf1f 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; }; EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; }; EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */; }; + EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */; }; EC3565522BEFC65F00A4E0BF /* NSManagedObjectContext++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */; }; EC3565542BEFC6AD00A4E0BF /* View++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565532BEFC6AD00A4E0BF /* View++.swift */; }; EC3565562BEFC7B300A4E0BF /* NSManagedObject++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */; }; @@ -74,7 +75,7 @@ ECA738F42BE612A000A4542E /* Array++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F32BE612A000A4542E /* Array++.swift */; }; ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738F52BE612B700A4542E /* MTLDevice++.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; - ECA739082BE623F300A4542E /* PenDockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDockView.swift */; }; + ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; }; ECEC01A82BEE11BA006DA24C /* QuadShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECEC01A72BEE11BA006DA24C /* QuadShape.swift */; }; ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA151F2BEF21EF00455818 /* MemoObject.swift */; }; ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECFA15212BEF21F500455818 /* CanvasObject.swift */; }; @@ -89,6 +90,7 @@ EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = ""; }; EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = ""; }; EC0D14272BF7BF20009BFE5F /* ContextMenuViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuViewModifier.swift; sourceTree = ""; }; + EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = ""; }; EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = ""; }; EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = ""; }; EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = ""; }; @@ -154,7 +156,7 @@ ECA738F32BE612A000A4542E /* Array++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array++.swift"; sourceTree = ""; }; ECA738F52BE612B700A4542E /* MTLDevice++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MTLDevice++.swift"; sourceTree = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; - ECA739072BE623F300A4542E /* PenDockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDockView.swift; sourceTree = ""; }; + ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = ""; }; ECEC01A72BEE11BA006DA24C /* QuadShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuadShape.swift; sourceTree = ""; }; ECFA151F2BEF21EF00455818 /* MemoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoObject.swift; sourceTree = ""; }; ECFA15212BEF21F500455818 /* CanvasObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasObject.swift; sourceTree = ""; }; @@ -199,6 +201,14 @@ path = Views; sourceTree = ""; }; + EC1B783B2BFA0AAC005A34E2 /* Toolbar */ = { + isa = PBXGroup; + children = ( + EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */, + ); + path = Toolbar; + sourceTree = ""; + }; EC5050042BF65CBC00B4D86E /* Core */ = { isa = PBXGroup; children = ( @@ -210,7 +220,7 @@ EC5050052BF65CCD00B4D86E /* PenDock */ = { isa = PBXGroup; children = ( - ECA739072BE623F300A4542E /* PenDockView.swift */, + ECA739072BE623F300A4542E /* PenDock.swift */, EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */, ); path = PenDock; @@ -326,6 +336,7 @@ ECA7387B2BE5EF3500A4542E /* Memo */ = { isa = PBXGroup; children = ( + EC1B783B2BFA0AAC005A34E2 /* Toolbar */, EC5050082BF65D0500B4D86E /* Memo */, EC5050052BF65CCD00B4D86E /* PenDock */, ); @@ -634,6 +645,8 @@ dependencies = ( ); name = Memola; + packageProductDependencies = ( + ); productName = Memola; productReference = EC7F6BE82BE5E6E300A34A7B /* Memola.app */; productType = "com.apple.product-type.application"; @@ -662,6 +675,8 @@ Base, ); mainGroup = EC7F6BDF2BE5E6E300A34A7B; + packageReferences = ( + ); productRefGroup = EC7F6BE92BE5E6E300A34A7B /* Products */; projectDirPath = ""; projectRoot = ""; @@ -691,6 +706,7 @@ ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */, ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, + EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */, ECA738E02BE610B900A4542E /* EraserRenderPass.swift in Sources */, EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */, ECA738912BE600F500A4542E /* Cache.metal in Sources */, @@ -719,7 +735,7 @@ ECA738862BE5FF2500A4542E /* Canvas.swift in Sources */, ECA738882BE5FF4400A4542E /* Renderer.swift in Sources */, ECA738D42BE60F9100A4542E /* StrokeGenerator.swift in Sources */, - ECA739082BE623F300A4542E /* PenDockView.swift in Sources */, + ECA739082BE623F300A4542E /* PenDock.swift in Sources */, ECA738CB2BE60F1900A4542E /* ViewPortContext.swift in Sources */, ECA738EE2BE6125D00A4542E /* simd_float4x4++.swift in Sources */, ECA7388C2BE6009600A4542E /* Textures.swift in Sources */, diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index a2471c5..b2787a0 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -24,17 +24,20 @@ 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 + @Published var locksCanvas: Bool = false + + 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 +95,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..818c762 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -162,6 +162,18 @@ extension CanvasViewController { } .store(in: &cancellables) + canvas.zoomPublisher + .sink { [weak self] zoomScale in + self?.zoomChanged(zoomScale) + } + .store(in: &cancellables) + + canvas.$locksCanvas + .sink { [weak self] state in + self?.lockModeChanged(state) + } + .store(in: &cancellables) + tool.$selectedPen .sink { [weak self] pen in self?.penChanged(to: pen) @@ -298,6 +310,17 @@ extension CanvasViewController { } } +extension CanvasViewController { + func zoomChanged(_ zoomScale: CGFloat) { + scrollView.setZoomScale(zoomScale, animated: true) + } + + func lockModeChanged(_ state: Bool) { + scrollView.isScrollEnabled = !state + scrollView.pinchGestureRecognizer?.isEnabled = !state + } +} + 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 53c2591..550b472 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -9,16 +9,19 @@ import SwiftUI import CoreData struct MemoView: View { - @Environment(\.dismiss) var dismiss - @StateObject var tool: Tool @StateObject var canvas: Canvas @StateObject var history = History() - let memo: MemoObject + @State var memo: MemoObject + @State var title: String + @FocusState var textFieldState: Bool + + let size: CGFloat = 32 init(memo: MemoObject) { self.memo = memo + self.title = memo.title self._tool = StateObject(wrappedValue: Tool(object: memo.tool)) self._canvas = StateObject(wrappedValue: Canvas(size: memo.canvas.size, canvasID: memo.canvas.objectID)) } @@ -26,35 +29,23 @@ struct MemoView: View { var body: some View { CanvasView() .ignoresSafeArea() - .overlay(alignment: .topTrailing) { - historyTool - .padding() - } .overlay(alignment: .trailing) { - PenDockView() - .frame(maxHeight: .infinity) - .padding() + PenDock() } - .overlay(alignment: .topLeading) { - Button { - closeMemo() - } label: { - Image(systemName: "xmark") - .contentShape(.circle) - .padding(15) - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 20)) - } - .hoverEffect(.lift) - .padding() + .overlay(alignment: .bottomLeading) { + zoomControl + } + .disabled(textFieldState) + .overlay(alignment: .top) { + Toolbar(memo: memo, size: size) } .disabled(canvas.state == .loading || canvas.state == .closing) .overlay { switch canvas.state { case .loading: - progressView("Loading memo...") + loadingIndicator("Loading memo...") case .closing: - progressView("Saving memo...") + loadingIndicator("Saving memo...") default: EmptyView() } @@ -64,31 +55,44 @@ struct MemoView: View { .environmentObject(history) } - var historyTool: some View { - HStack { - Button { - history.historyPublisher.send(.undo) + @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] + if !canvas.locksCanvas { + 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: { - Image(systemName: "arrow.uturn.backward.circle") - .contentShape(.circle) + 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) } - .hoverEffect(.lift) - .disabled(history.undoDisabled) - Button { - history.historyPublisher.send(.redo) - } label: { - Image(systemName: "arrow.uturn.forward.circle") - .contentShape(.circle) - } - .hoverEffect(.lift) - .disabled(history.redoDisabled) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) } - .padding(15) - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 20)) } - func progressView(_ title: String) -> some View { + func loadingIndicator(_ title: String) -> some View { ProgressView { Text(title) } @@ -97,11 +101,4 @@ struct MemoView: View { .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 20)) } - - func closeMemo() { - withPersistenceSync(\.viewContext) { context in - try context.saveIfNeeded() - } - dismiss() - } } diff --git a/Memola/Features/Memo/PenDock/PenDockView.swift b/Memola/Features/Memo/PenDock/PenDock.swift similarity index 77% rename from Memola/Features/Memo/PenDock/PenDockView.swift rename to Memola/Features/Memo/PenDock/PenDock.swift index eeace76..1e02b77 100644 --- a/Memola/Features/Memo/PenDock/PenDockView.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -1,5 +1,5 @@ // -// PenDockView.swift +// PenDock.swift // Memola // // Created by Dscyre Scotti on 5/4/24. @@ -7,45 +7,36 @@ import SwiftUI -struct PenDockView: View { +struct PenDock: View { @EnvironmentObject var tool: Tool + @EnvironmentObject var canvas: Canvas let width: CGFloat = 90 let height: CGFloat = 30 - let factor: CGFloat = 0.95 + let factor: CGFloat = 0.9 @State var refreshScrollId: UUID = UUID() @State var opensColorPicker: Bool = false var body: some View { - VStack(alignment: .trailing) { - if let pen = tool.selectedPen { - VStack(spacing: 5) { - penColorView(pen) - penThicknessView(pen) - } - .padding(10) - .frame(width: width * factor - 18) - .background { - RoundedRectangle(cornerRadius: 20) - .fill(.regularMaterial) - } - .transition(.move(edge: .trailing).combined(with: .opacity)) - } else { - Color.clear - .frame(width: width * factor - 18, height: 50) + if !canvas.locksCanvas { + VStack(alignment: .trailing) { + penPropertyTool + penItemList } - penScrollView + .fixedSize() + .frame(maxHeight: .infinity) + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } - .fixedSize() } - var penScrollView: some View { + var penItemList: some View { ScrollViewReader { proxy in ScrollView(.vertical, showsIndicators: false) { LazyVStack(spacing: 0) { ForEach(tool.pens) { pen in - penView(pen) + penItemRow(pen) .id(pen.id) .scrollTransition { content, phase in content @@ -68,20 +59,20 @@ struct PenDockView: View { .frame(maxHeight:( (height * factor + 10) * 6) + 20) .fixedSize() .background(alignment: .trailing) { - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) .frame(width: width * factor - 18) } - .clipShape(.rect(cornerRadii: .init(bottomTrailing: 20, topTrailing: 20))) + .clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8))) .overlay(alignment: .bottomLeading) { newPenButton .offset(x: 60, y: 10) } } - func penView(_ pen: Pen) -> some View { + func penItemRow(_ pen: Pen) -> some View { ZStack { - penShadowView(pen) + penShadow(pen) if let tip = pen.style.icon.tip { Image(tip) .resizable() @@ -139,7 +130,7 @@ struct PenDockView: View { } .controlGroupStyle(.menu) } preview: { - penPreviewView(pen) + penPreview(pen) .drawingGroup() .contentShape(.contextMenuPreview, .rect(cornerRadius: 10)) } @@ -147,14 +138,36 @@ struct PenDockView: View { tool.draggedPen = pen return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider() } preview: { - penPreviewView(pen) + penPreview(pen) .contentShape(.dragPreview, .rect(cornerRadius: 10)) } .onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool, action: { refreshScrollId = UUID() })) .offset(x: tool.selectedPen === pen ? 0 : 25) } - func penColorView(_ pen: Pen) -> some View { + @ViewBuilder + var penPropertyTool: some View { + if let pen = tool.selectedPen { + VStack(spacing: 5) { + if pen.strokeStyle == .marker { + penColorPicker(pen) + } + penThicknessPicker(pen) + } + .padding(10) + .frame(width: width * factor - 18) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } else { + Color.clear + .frame(width: width * factor - 18, height: 50) + } + } + + func penColorPicker(_ pen: Pen) -> some View { Button { opensColorPicker = true } label: { @@ -174,16 +187,16 @@ struct PenDockView: View { } } .background(baseColor) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .frame(height: 28) + .clipShape(.rect(cornerRadius: 8)) + .frame(height: 25) .overlay { - RoundedRectangle(cornerRadius: 10) + RoundedRectangle(cornerRadius: 8) .stroke(Color.gray, lineWidth: 0.4) } .padding(0.2) - .padding(.top, 4) .drawingGroup() } + .buttonStyle(.plain) .hoverEffect(.lift) .popover(isPresented: $opensColorPicker) { let color = Binding( @@ -195,15 +208,20 @@ struct PenDockView: View { ) ColorPicker(color: color) .presentationCompactAdaptation(.popover) + .onDisappear { + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } } } @ViewBuilder - func penThicknessView(_ pen: Pen) -> some View { + func penThicknessPicker(_ pen: Pen) -> some View { let minimum: CGFloat = pen.style.thickness.min let maximum: CGFloat = pen.style.thickness.max - let start: CGFloat = 5 - let end: CGFloat = 15 + let start: CGFloat = 4 + let end: CGFloat = 10 let selection = Binding( get: { pen.thickness }, set: { @@ -213,20 +231,20 @@ struct PenDockView: View { ) Picker("", selection: selection) { ForEach(pen.style.thicknessSteps, id: \.self) { step in - let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (1 / step) - if pen.thickness == step { - Circle() - .fill(.black) - .frame(width: size, height: size) - } else { - Circle() - .stroke(Color.black, lineWidth: 1) - .frame(width: size, height: size) - } + let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step) + Circle() + .fill(.black) + .frame(width: size, height: size) + .frame(width: size + 2, height: size + 2) } } .pickerStyle(.wheel) - .frame(width: width * factor - 18, height: 40) + .frame(width: width * factor - 18, height: 35) + .onChange(of: pen.thickness) { _, _ in + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } } var newPenButton: some View { @@ -254,7 +272,7 @@ struct PenDockView: View { .hoverEffect(.lift) } - func penPreviewView(_ pen: Pen) -> some View { + func penPreview(_ pen: Pen) -> some View { ZStack { if let tip = pen.style.icon.tip { Image(tip) @@ -270,7 +288,7 @@ struct PenDockView: View { .padding(.leading, 10) } - func penShadowView(_ pen: Pen) -> some View { + func penShadow(_ pen: Pen) -> some View { ZStack { Group { if let tip = pen.style.icon.tip { diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift new file mode 100644 index 0000000..f7ac714 --- /dev/null +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -0,0 +1,136 @@ +// +// Toolbar.swift +// Memola +// +// Created by Dscyre Scotti on 5/19/24. +// + +import SwiftUI +import Foundation + +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 + + let size: CGFloat + + init(memo: MemoObject, size: CGFloat) { + self.memo = memo + self.size = size + self.title = memo.title + } + + var body: some View { + HStack(spacing: 5) { + if !canvas.locksCanvas { + closeButton + titleField + } + Spacer() + if !canvas.locksCanvas { + historyControl + } + lockButton + } + .font(.subheadline) + .padding(10) + } + + var closeButton: some View { + Button { + closeMemo() + } label: { + Image(systemName: "xmark") + .contentShape(.circle) + .frame(width: size, height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + .disabled(textFieldState) + .transition(.move(edge: .top).combined(with: .blurReplace)) + } + + var titleField: some View { + TextField("", text: $title) + .focused($textFieldState) + .textFieldStyle(.plain) + .padding(.horizontal, size / 2.5) + .frame(width: 140, height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .onChange(of: textFieldState) { oldValue, newValue in + if !newValue { + if !title.isEmpty { + memo.title = title + } else { + title = memo.title + } + } + } + .transition(.move(edge: .top).combined(with: .blurReplace)) + } + + var historyControl: some View { + HStack { + Button { + history.historyPublisher.send(.undo) + } label: { + Image(systemName: "arrow.uturn.backward.circle") + + .contentShape(.circle) + } + .hoverEffect(.lift) + .disabled(history.undoDisabled) + Button { + history.historyPublisher.send(.redo) + } label: { + Image(systemName: "arrow.uturn.forward.circle") + .contentShape(.circle) + } + .hoverEffect(.lift) + .disabled(history.redoDisabled) + } + .frame(width: size * 2, height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .disabled(textFieldState) + .transition(.move(edge: .top).combined(with: .blurReplace)) + } + + var lockButton: some View { + Button { + withAnimation { + canvas.locksCanvas.toggle() + } + } label: { + ZStack { + if canvas.locksCanvas { + Image(systemName: "lock.open") + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } else { + Image(systemName: "lock") + .transition(.move(edge: .leading).combined(with: .blurReplace)) + } + } + .contentShape(.circle) + .frame(width: size, height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + } + + func closeMemo() { + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() + } + dismiss() + } +} diff --git a/Memola/Features/Memos/MemosView.swift b/Memola/Features/Memos/MemosView.swift index 4f66f0d..331016c 100644 --- a/Memola/Features/Memos/MemosView.swift +++ b/Memola/Features/Memos/MemosView.swift @@ -76,8 +76,12 @@ struct MemosView: View { let eraserPenObject = PenObject.createObject(\.viewContext, penStyle: .eraser) eraserPenObject.orderIndex = 0 - let markerPenObject = PenObject.createObject(\.viewContext, penStyle: .marker) - markerPenObject.orderIndex = 1 + let markerPenObjects = [Color.red, Color.blue, Color.yellow, Color.black].enumerated().map { (index, color) in + let penObject = PenObject.createObject(\.viewContext, penStyle: .marker) + penObject.orderIndex = Int16(index) + 1 + penObject.color = color.components + return penObject + } let graphicContextObject = GraphicContextObject(\.viewContext) graphicContextObject.strokes = [] @@ -89,10 +93,10 @@ struct MemosView: View { canvasObject.graphicContext = graphicContextObject toolObject.memo = memoObject - toolObject.pens = [eraserPenObject, markerPenObject] + toolObject.pens = .init(array: [eraserPenObject] + markerPenObjects) eraserPenObject.tool = toolObject - markerPenObject.tool = toolObject + markerPenObjects.forEach { $0.tool = toolObject } graphicContextObject.canvas = canvasObject