diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index c5886b2..89feef7 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -1254,6 +1254,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Memola/Memola.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\""; @@ -1291,6 +1292,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Memola/Memola.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Memola/Preview Content\""; diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index 2c58dbf..663b9a0 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -162,7 +162,13 @@ extension Canvas { // MARK: - Zoom Scale extension Canvas { func setZoomScale(_ zoomScale: CGFloat) { + #if os(macOS) self.zoomScale = min(max(zoomScale, minimumZoomScale), maximumZoomScale) + #else + DispatchQueue.main.async { [unowned self] in + self.zoomScale = min(max(zoomScale, minimumZoomScale), maximumZoomScale) + } + #endif } } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 074536e..93a2fea 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -168,16 +168,16 @@ extension CanvasViewController { let height = size.height * scale let newFrame = CGRect(x: 0, y: 0, width: width, height: height) drawingView.frame = newFrame - + + #if os(macOS) DispatchQueue.main.async { [unowned canvas] in canvas.setZoomScale(canvas.defaultZoomScale) } - - #if os(macOS) scrollView.contentView.setBoundsSize(newFrame.size) let center = NSPoint(x: newFrame.midX, y: newFrame.midY) scrollView.setMagnification(canvas.defaultZoomScale, centeredAt: center) #else + canvas.setZoomScale(canvas.defaultZoomScale) scrollView.setZoomScale(canvas.defaultZoomScale, animated: true) centerDocumentView(to: newSize) #endif @@ -427,9 +427,6 @@ extension CanvasViewController { } func draggingStarted() { - #if os(macOS) - canvas.setZoomScale(scrollView.magnification) - #endif guard !renderer.updatesViewPort else { return } canvas.updateClipBounds(scrollView, on: drawingView) drawingView.disableUserInteraction() @@ -437,9 +434,6 @@ extension CanvasViewController { } func draggingEnded() { - #if os(macOS) - canvas.setZoomScale(scrollView.magnification) - #endif renderer.setUpdatesViewPort(false) renderer.setRedrawsGraphicRender() renderView.draw() diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift index 3d2873c..e1c45bd 100644 --- a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -23,6 +23,9 @@ struct ElementToolbar: View { var body: some View { Group { + #if os(macOS) + regularToolbar + #else if horizontalSizeClass == .regular { regularToolbar } else { @@ -42,6 +45,7 @@ struct ElementToolbar: View { } .padding(.bottom, 10) } + #endif } #if os(iOS) .fullScreenCover(isPresented: $opensCamera) { @@ -94,9 +98,12 @@ struct ElementToolbar: View { .frame(width: size, height: size) .foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif .background { if tool.selection == .hand { @@ -116,9 +123,12 @@ struct ElementToolbar: View { .frame(width: size, height: size) .foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif .background { if tool.selection == .pen { @@ -138,9 +148,12 @@ struct ElementToolbar: View { .frame(width: size, height: size) .foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif .background { if tool.selection == .photo { @@ -159,13 +172,13 @@ struct ElementToolbar: View { .transition(.blurReplace.animation(.easeIn(duration: 0.1))) } } - .background { - if tool.selection == .photo { - RoundedRectangle(cornerRadius: 8) - .fill(Color.white.tertiary) - .transition(.move(edge: .leading).combined(with: .opacity).animation(.easeIn(duration: 0.1))) - } - } +// .background { +// if tool.selection == .photo { +// RoundedRectangle(cornerRadius: 8) +// .fill(Color.white.tertiary) +// .transition(.move(edge: .leading).combined(with: .opacity).animation(.easeIn(duration: 0.1))) +// } +// } } .background { RoundedRectangle(cornerRadius: 8) @@ -222,18 +235,24 @@ struct ElementToolbar: View { .contentShape(.circle) .frame(width: size, height: size) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { Image(systemName: "photo.fill.on.rectangle.fill") .contentShape(.circle) .frame(width: size, height: size) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif } } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 13c9d2d..9ebb954 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -31,11 +31,15 @@ struct MemoView: View { var body: some View { Group { + #if os(macOS) + canvasView + #else if horizontalSizeClass == .regular { canvasView } else { compactCanvasView } + #endif } .overlay(alignment: .top) { Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history) @@ -66,7 +70,6 @@ struct MemoView: View { switch tool.selection { case .pen: PenDock(tool: tool, canvas: canvas, size: size) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) case .photo: if let photoItem = tool.selectedPhotoItem { PhotoPreview(photoItem: photoItem, tool: tool) @@ -106,7 +109,7 @@ struct MemoView: View { } } .overlay(alignment: .bottom) { - if tool.selection != .hand { + if tool.selection != .hand && !canvas.locksCanvas { Button { withAnimation { tool.selectTool(.hand) @@ -121,6 +124,7 @@ struct MemoView: View { .contentShape(.capsule) } .offset(y: 5) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) } } } @@ -132,6 +136,40 @@ struct MemoView: View { 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 { + #if os(macOS) + 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) + .font(.subheadline) + .frame(height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + .menuIndicator(.hidden) + .frame(width: 50, height: size) + .padding(.leading, 12) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + .menuStyle(.borderlessButton) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + .padding(10) + #else Menu { ForEach(zoomScales, id: \.self) { scale in Button { @@ -156,12 +194,12 @@ struct MemoView: View { .frame(height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) .padding(10) } - #if os(iOS) .hoverEffect(.lift) - #endif .transition(.move(edge: .bottom).combined(with: .blurReplace)) + #endif } } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index 6c90d29..58a7b0a 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -23,8 +23,28 @@ struct PenDock: View { @State var refreshScrollId: UUID = UUID() @State var opensColorPicker: Bool = false + #if os(macOS) + @State var showsThinknessPicker: Bool = false + #endif var body: some View { + #if os(macOS) + ZStack(alignment: .bottomTrailing) { + if !canvas.locksCanvas { + VStack(alignment: .trailing) { + penPropertyTool + penItemList + } + .fixedSize() + .frame(maxHeight: .infinity) + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } + lockButton + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } + #else if horizontalSizeClass == .regular { ZStack(alignment: .bottomTrailing) { if !canvas.locksCanvas { @@ -73,6 +93,7 @@ struct PenDock: View { .transition(.move(edge: .trailing).combined(with: .blurReplace)) } } + #endif } @ViewBuilder @@ -319,12 +340,12 @@ struct PenDock: View { var compactPenPropertyTool: some View { if let pen = tool.selectedPen { HStack(spacing: 10) { - compactPenThicknessPicker(pen) - .frame(width: width) + penThicknessPicker(pen) + .frame(width: size) .rotationEffect(.degrees(-90)) if pen.strokeStyle == .marker { penColorPicker(pen) - .frame(width: width) + .frame(width: size) .transition(.move(edge: .trailing).combined(with: .opacity)) } } @@ -353,7 +374,8 @@ struct PenDock: View { } .background(baseColor) .clipShape(.rect(cornerRadius: 8)) - .frame(height: horizontalSizeClass == .compact ? 30 : 25) + .contentShape(.rect(cornerRadius: 8)) + .frame(height: size) .overlay { RoundedRectangle(cornerRadius: 8) .stroke(Color.gray, lineWidth: 0.4) @@ -389,64 +411,50 @@ struct PenDock: View { let maximum: CGFloat = pen.style.thickness.max let start: CGFloat = 4 let end: CGFloat = 10 - let selection = Binding( - get: { pen.thickness }, - set: { - pen.thickness = $0 - tool.objectWillChange.send() - } - ) - Picker("", selection: selection) { - ForEach(pen.style.thicknessSteps, id: \.self) { step in - 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) - } - } - #if os(iOS) - .hoverEffect(.lift) - .pickerStyle(.wheel) - #endif - .frame(width: width * factor - 18, height: 35) - .onChange(of: pen.thickness) { _, _ in - withPersistence(\.viewContext) { context in - try context.saveIfNeeded() - } - } - } - - @ViewBuilder - func compactPenThicknessPicker(_ pen: Pen) -> some View { - let minimum: CGFloat = pen.style.thickness.min - let maximum: CGFloat = pen.style.thickness.max - let start: CGFloat = 4 - let end: CGFloat = 7 - let selection = Binding( + let selection = Binding( get: { pen.thickness }, set: { - pen.thickness = $0 + pen.thickness = $0 ?? .zero tool.objectWillChange.send() } ) - Picker("", selection: selection) { - ForEach(pen.style.thicknessSteps, id: \.self) { step in - 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) - } - } - #if os(iOS) - .hoverEffect(.lift) - .pickerStyle(.wheel) + #if os(macOS) + let _width = width * factor - 38 + #else + let _width = horizontalSizeClass == .compact ? self.size : width * factor - 38 #endif - .frame(width: 50, height: 30) - .onChange(of: pen.thickness) { _, _ in - withPersistence(\.viewContext) { context in - try context.saveIfNeeded() + ScrollViewReader { proxy in + ScrollView(showsIndicators: false) { + LazyVStack(spacing: 0) { + ForEach(pen.style.thicknessSteps, id: \.self) { step in + let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step) + Circle() + .foregroundStyle(.primary) + .frame(width: size, height: size) + .frame(width: _width, height: self.size) + .contentShape(.rect) + .id(step) + } + } + } + #if os(macOS) + .frame(height: size) + #else + .frame(width: _width, height: size) + #endif + .background(.gray.quaternary) + .clipShape(.rect(cornerRadius: 8)) + .scrollPosition(id: selection, anchor: .center) + .scrollTargetLayout() + .scrollTargetBehavior(.viewAligned) + .scrollIndicators(.hidden) + .onAppear { + proxy.scrollTo(selection.wrappedValue) + } + .onChange(of: pen.thickness) { _, _ in + withPersistence(\.viewContext) { context in + try context.saveIfNeeded() + } } } } @@ -467,6 +475,8 @@ struct PenDock: View { .foregroundStyle(.green) #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif } @@ -541,13 +551,15 @@ struct PenDock: View { } } label: { Image(systemName: canvas.locksCanvas ? "lock.fill" : "lock.open.fill") - .contentShape(.circle) .frame(width: size, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif .contentTransition(.symbolEffect(.replace)) } diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 8ea359e..05dc21c 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -41,9 +41,15 @@ struct Toolbar: View { } } .frame(maxWidth: .infinity, alignment: .leading) + #if os(macOS) + if !canvas.locksCanvas { + ElementToolbar(size: size, tool: tool, canvas: canvas) + } + #else if !canvas.locksCanvas, horizontalSizeClass == .regular { ElementToolbar(size: size, tool: tool, canvas: canvas) } + #endif HStack(spacing: 5) { if !canvas.locksCanvas { gridModeControl @@ -61,13 +67,15 @@ struct Toolbar: View { closeMemo() } label: { Image(systemName: "xmark") - .contentShape(.circle) .frame(width: size, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif .disabled(textFieldState) .transition(.move(edge: .top).combined(with: .blurReplace)) @@ -98,29 +106,34 @@ struct Toolbar: View { } var historyControl: some View { - HStack { + HStack(spacing: 0) { Button { history.historyPublisher.send(.undo) } label: { Image(systemName: "arrow.uturn.backward.circle") - .contentShape(.circle) + .frame(width: size, height: size) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif .disabled(history.undoDisabled) Button { history.historyPublisher.send(.redo) } label: { Image(systemName: "arrow.uturn.forward.circle") - .contentShape(.circle) + .frame(width: size, height: size) + .contentShape(.rect(cornerRadius: 8)) } #if os(iOS) .hoverEffect(.lift) + #else + .buttonStyle(.plain) #endif .disabled(history.redoDisabled) } - .frame(width: size * 2, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) .disabled(textFieldState) @@ -128,6 +141,27 @@ struct Toolbar: View { } var gridModeControl: some View { + #if os(macOS) + Button { + switch canvas.gridMode { + case .none: + canvas.gridMode = .point + case .point: + canvas.gridMode = .line + case .line: + canvas.gridMode = .none + } + } label: { + Image(systemName: canvas.gridMode.icon) + .frame(width: size, height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + .buttonStyle(.plain) + .contentTransition(.symbolEffect(.replace)) + .transition(.move(edge: .top).combined(with: .blurReplace)) + #else Menu { ForEach(GridMode.all, id: \.self) { mode in Button { @@ -143,16 +177,15 @@ struct Toolbar: View { } } label: { Image(systemName: canvas.gridMode.icon) - .contentShape(.circle) .frame(width: size, height: size) .background(.regularMaterial) .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) } - #if os(iOS) .hoverEffect(.lift) - #endif .contentTransition(.symbolEffect(.replace)) .transition(.move(edge: .top).combined(with: .blurReplace)) + #endif } func closeMemo() {