From 2e90a5c8eb49f728a78ef66912f7df5013f8c52d Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 19 May 2024 17:28:35 +0700 Subject: [PATCH 1/7] feat: redesign memo tool bar --- Memola.xcodeproj/project.pbxproj | 12 +++ Memola/Features/Memo/Memo/MemoView.swift | 57 ++---------- Memola/Features/Memo/Toolbar/Toolbar.swift | 101 +++++++++++++++++++++ 3 files changed, 120 insertions(+), 50 deletions(-) create mode 100644 Memola/Features/Memo/Toolbar/Toolbar.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 6f98d97..dbdbf40 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 */; }; @@ -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 = ""; }; @@ -199,6 +201,14 @@ path = Views; sourceTree = ""; }; + EC1B783B2BFA0AAC005A34E2 /* Toolbar */ = { + isa = PBXGroup; + children = ( + EC1B783C2BFA0AC9005A34E2 /* Toolbar.swift */, + ); + path = Toolbar; + sourceTree = ""; + }; EC5050042BF65CBC00B4D86E /* Core */ = { isa = PBXGroup; children = ( @@ -326,6 +336,7 @@ ECA7387B2BE5EF3500A4542E /* Memo */ = { isa = PBXGroup; children = ( + EC1B783B2BFA0AAC005A34E2 /* Toolbar */, EC5050082BF65D0500B4D86E /* Memo */, EC5050052BF65CCD00B4D86E /* PenDock */, ); @@ -691,6 +702,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 */, diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 53c2591..17edf02 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -9,16 +9,17 @@ 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 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,27 +27,14 @@ struct MemoView: View { var body: some View { CanvasView() .ignoresSafeArea() - .overlay(alignment: .topTrailing) { - historyTool - .padding() - } .overlay(alignment: .trailing) { PenDockView() .frame(maxHeight: .infinity) .padding() } - .overlay(alignment: .topLeading) { - Button { - closeMemo() - } label: { - Image(systemName: "xmark") - .contentShape(.circle) - .padding(15) - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 20)) - } - .hoverEffect(.lift) - .padding() + .disabled(textFieldState) + .overlay(alignment: .top) { + Toolbar(memo: memo) } .disabled(canvas.state == .loading || canvas.state == .closing) .overlay { @@ -64,30 +52,6 @@ struct MemoView: View { .environmentObject(history) } - var historyTool: 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) - } - .padding(15) - .background(.regularMaterial) - .clipShape(RoundedRectangle(cornerRadius: 20)) - } - func progressView(_ title: String) -> some View { ProgressView { Text(title) @@ -97,11 +61,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/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift new file mode 100644 index 0000000..80a82de --- /dev/null +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -0,0 +1,101 @@ +// +// 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 + + @State var memo: MemoObject + @State var title: String + @FocusState var textFieldState: Bool + + init(memo: MemoObject) { + self.memo = memo + self.title = memo.title + } + + var body: some View { + HStack(spacing: 5) { + closeButton + titleField + Spacer() + historyTool + } + .font(.subheadline) + .padding(10) + } + + var closeButton: some View { + Button { + closeMemo() + } label: { + Image(systemName: "xmark") + .contentShape(.circle) + .padding(10) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + .disabled(textFieldState) + } + + var titleField: some View { + TextField("", text: $title) + .focused($textFieldState) + .textFieldStyle(.plain) + .padding(.vertical, 5) + .padding(.horizontal, 10) + .frame(width: 120) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .onChange(of: textFieldState) { oldValue, newValue in + if !newValue { + if !title.isEmpty { + memo.title = title + } else { + title = memo.title + } + } + } + } + + var historyTool: 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) + } + .padding(10) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .disabled(textFieldState) + } + + func closeMemo() { + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() + } + dismiss() + } +} From e70af36235b2266fa2e8b89c36f36cb13211d57f Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 19 May 2024 17:45:13 +0700 Subject: [PATCH 2/7] refactor: rename view variables and methods --- Memola.xcodeproj/project.pbxproj | 8 +-- Memola/Features/Memo/Memo/MemoView.swift | 8 +-- .../{PenDockView.swift => PenDock.swift} | 63 ++++++++++--------- 3 files changed, 42 insertions(+), 37 deletions(-) rename Memola/Features/Memo/PenDock/{PenDockView.swift => PenDock.swift} (89%) diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index dbdbf40..076c6cb 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -75,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 */; }; @@ -156,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 = ""; }; @@ -220,7 +220,7 @@ EC5050052BF65CCD00B4D86E /* PenDock */ = { isa = PBXGroup; children = ( - ECA739072BE623F300A4542E /* PenDockView.swift */, + ECA739072BE623F300A4542E /* PenDock.swift */, EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */, ); path = PenDock; @@ -731,7 +731,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/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 17edf02..ec5c4cc 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -28,7 +28,7 @@ struct MemoView: View { CanvasView() .ignoresSafeArea() .overlay(alignment: .trailing) { - PenDockView() + PenDock() .frame(maxHeight: .infinity) .padding() } @@ -40,9 +40,9 @@ struct MemoView: View { .overlay { switch canvas.state { case .loading: - progressView("Loading memo...") + loadingIndicator("Loading memo...") case .closing: - progressView("Saving memo...") + loadingIndicator("Saving memo...") default: EmptyView() } @@ -52,7 +52,7 @@ struct MemoView: View { .environmentObject(history) } - func progressView(_ title: String) -> some View { + func loadingIndicator(_ title: String) -> some View { ProgressView { Text(title) } diff --git a/Memola/Features/Memo/PenDock/PenDockView.swift b/Memola/Features/Memo/PenDock/PenDock.swift similarity index 89% rename from Memola/Features/Memo/PenDock/PenDockView.swift rename to Memola/Features/Memo/PenDock/PenDock.swift index eeace76..d478839 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,7 +7,7 @@ import SwiftUI -struct PenDockView: View { +struct PenDock: View { @EnvironmentObject var tool: Tool let width: CGFloat = 90 @@ -19,33 +19,18 @@ struct PenDockView: View { 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) - } - penScrollView + penPropertyTool + penItemList } .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 @@ -79,9 +64,9 @@ struct PenDockView: View { } } - 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 +124,7 @@ struct PenDockView: View { } .controlGroupStyle(.menu) } preview: { - penPreviewView(pen) + penPreview(pen) .drawingGroup() .contentShape(.contextMenuPreview, .rect(cornerRadius: 10)) } @@ -147,14 +132,34 @@ 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) { + penColorPicker(pen) + penThicknessPicker(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) + } + } + + func penColorPicker(_ pen: Pen) -> some View { Button { opensColorPicker = true } label: { @@ -199,7 +204,7 @@ struct PenDockView: View { } @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 @@ -254,7 +259,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 +275,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 { From 3b0d93477e0ebb77eb8e8f677be7c24f6460ff11 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 19 May 2024 21:30:08 +0700 Subject: [PATCH 3/7] feat: make pen dock compact --- Memola.xcodeproj/project.pbxproj | 4 +++ Memola/Features/Memo/Memo/MemoView.swift | 2 -- Memola/Features/Memo/PenDock/PenDock.swift | 40 ++++++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 076c6cb..258bf1f 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -645,6 +645,8 @@ dependencies = ( ); name = Memola; + packageProductDependencies = ( + ); productName = Memola; productReference = EC7F6BE82BE5E6E300A34A7B /* Memola.app */; productType = "com.apple.product-type.application"; @@ -673,6 +675,8 @@ Base, ); mainGroup = EC7F6BDF2BE5E6E300A34A7B; + packageReferences = ( + ); productRefGroup = EC7F6BE92BE5E6E300A34A7B /* Products */; projectDirPath = ""; projectRoot = ""; diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index ec5c4cc..c6a1603 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -29,8 +29,6 @@ struct MemoView: View { .ignoresSafeArea() .overlay(alignment: .trailing) { PenDock() - .frame(maxHeight: .infinity) - .padding() } .disabled(textFieldState) .overlay(alignment: .top) { diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index d478839..f6d8f55 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -12,7 +12,7 @@ struct PenDock: View { 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 @@ -23,6 +23,8 @@ struct PenDock: View { penItemList } .fixedSize() + .frame(maxHeight: .infinity) + .padding(10) } var penItemList: some View { @@ -53,11 +55,11 @@ struct PenDock: 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) @@ -143,13 +145,15 @@ struct PenDock: View { var penPropertyTool: some View { if let pen = tool.selectedPen { VStack(spacing: 5) { - penColorPicker(pen) + if pen.strokeStyle == .marker { + penColorPicker(pen) + } penThicknessPicker(pen) } .padding(10) .frame(width: width * factor - 18) .background { - RoundedRectangle(cornerRadius: 20) + RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) } .transition(.move(edge: .trailing).combined(with: .opacity)) @@ -179,14 +183,13 @@ struct PenDock: 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() } .hoverEffect(.lift) @@ -207,8 +210,8 @@ struct PenDock: 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: { @@ -219,19 +222,14 @@ struct PenDock: 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) - } + 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) } var newPenButton: some View { From 20ed32b1860139440617631e5b3bf1ed3754fc47 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 19 May 2024 21:43:06 +0700 Subject: [PATCH 4/7] feat: add more pens in newly created memo object --- Memola/Features/Memo/PenDock/PenDock.swift | 2 +- Memola/Features/Memos/MemosView.swift | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index f6d8f55..f660462 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -221,7 +221,7 @@ struct PenDock: View { ) Picker("", selection: selection) { ForEach(pen.style.thicknessSteps, id: \.self) { step in - let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (1 / step) + let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step) Circle() .fill(.black) .frame(width: size, height: size) 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 From 095e5fa515c93b90375861c177029d97f66ff661 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 19 May 2024 21:59:31 +0700 Subject: [PATCH 5/7] feat: save pen changes --- Memola/Features/Memo/PenDock/PenDock.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index f660462..e58c0ea 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -192,6 +192,7 @@ struct PenDock: View { .padding(0.2) .drawingGroup() } + .buttonStyle(.plain) .hoverEffect(.lift) .popover(isPresented: $opensColorPicker) { let color = Binding( @@ -203,6 +204,11 @@ struct PenDock: View { ) ColorPicker(color: color) .presentationCompactAdaptation(.popover) + .onDisappear { + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } } } @@ -230,6 +236,11 @@ struct PenDock: View { } .pickerStyle(.wheel) .frame(width: width * factor - 18, height: 35) + .onChange(of: pen.thickness) { _, _ in + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } + } } var newPenButton: some View { From 819c7dc321d3f3e260a1605e54f5461ab8b0df6e Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 19 May 2024 23:50:21 +0700 Subject: [PATCH 6/7] feat: add zoom scale options --- Memola/Canvas/Core/Canvas.swift | 13 ++++-- .../ViewController/CanvasViewController.swift | 12 ++++++ Memola/Features/Memo/Memo/MemoView.swift | 41 ++++++++++++++++++- Memola/Features/Memo/Toolbar/Toolbar.swift | 22 ++++++---- 4 files changed, 74 insertions(+), 14 deletions(-) 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) From 6208c5d62e8be7a4f960dbac7f61556ed4b389e2 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Mon, 20 May 2024 01:11:15 +0700 Subject: [PATCH 7/7] feat: add canvas lock button --- Memola/Canvas/Core/Canvas.swift | 1 + .../ViewController/CanvasViewController.swift | 11 +++++ Memola/Features/Memo/Memo/MemoView.swift | 45 ++++++++++--------- Memola/Features/Memo/PenDock/PenDock.swift | 18 +++++--- Memola/Features/Memo/Toolbar/Toolbar.swift | 37 +++++++++++++-- 5 files changed, 81 insertions(+), 31 deletions(-) diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index b0fda1c..b2787a0 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -29,6 +29,7 @@ final class Canvas: ObservableObject, Identifiable, @unchecked Sendable { @Published var state: State = .initial @Published var zoomScale: CGFloat = .zero + @Published var locksCanvas: Bool = false let zoomPublisher = PassthroughSubject() diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 1f255d7..818c762 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -168,6 +168,12 @@ extension CanvasViewController { } .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) @@ -308,6 +314,11 @@ extension CanvasViewController { func zoomChanged(_ zoomScale: CGFloat) { scrollView.setZoomScale(zoomScale, animated: true) } + + func lockModeChanged(_ state: Bool) { + scrollView.isScrollEnabled = !state + scrollView.pinchGestureRecognizer?.isEnabled = !state + } } extension CanvasViewController { diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index f1700fe..550b472 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -61,31 +61,34 @@ struct MemoView: View { 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") + 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) } - .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) } - } 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) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) } } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index e58c0ea..1e02b77 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -9,6 +9,7 @@ import SwiftUI struct PenDock: View { @EnvironmentObject var tool: Tool + @EnvironmentObject var canvas: Canvas let width: CGFloat = 90 let height: CGFloat = 30 @@ -18,13 +19,16 @@ struct PenDock: View { @State var opensColorPicker: Bool = false var body: some View { - VStack(alignment: .trailing) { - penPropertyTool - penItemList + if !canvas.locksCanvas { + VStack(alignment: .trailing) { + penPropertyTool + penItemList + } + .fixedSize() + .frame(maxHeight: .infinity) + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } - .fixedSize() - .frame(maxHeight: .infinity) - .padding(10) } var penItemList: some View { @@ -156,7 +160,7 @@ struct PenDock: View { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) } - .transition(.move(edge: .trailing).combined(with: .opacity)) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } else { Color.clear .frame(width: width * factor - 18, height: 50) diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index ba6500b..f7ac714 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -28,10 +28,15 @@ struct Toolbar: View { var body: some View { HStack(spacing: 5) { - closeButton - titleField + if !canvas.locksCanvas { + closeButton + titleField + } Spacer() - historyControl + if !canvas.locksCanvas { + historyControl + } + lockButton } .font(.subheadline) .padding(10) @@ -49,6 +54,7 @@ struct Toolbar: View { } .hoverEffect(.lift) .disabled(textFieldState) + .transition(.move(edge: .top).combined(with: .blurReplace)) } var titleField: some View { @@ -68,6 +74,7 @@ struct Toolbar: View { } } } + .transition(.move(edge: .top).combined(with: .blurReplace)) } var historyControl: some View { @@ -94,6 +101,30 @@ struct Toolbar: View { .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() {