Merge pull request #50 from dscyrescotti/feature/toolbar-fine-tune

Fine tune memo canvas tool bar items
This commit is contained in:
Aye Chan
2024-06-24 22:34:43 +08:00
committed by GitHub
6 changed files with 101 additions and 48 deletions
+7 -1
View File
@@ -20,8 +20,9 @@ public class Tool: NSObject, ObservableObject {
@Published var draggedPen: Pen? @Published var draggedPen: Pen?
// MARK: - Photo // MARK: - Photo
@Published var selectedPhotoItem: PhotoItem? @Published var selectedPhotoItem: PhotoItem?
@Published var isLoadingPhoto: Bool = false
@Published var selection: ToolSelection = .none @Published var selection: ToolSelection = .hand
let scrollPublisher = PassthroughSubject<String, Never>() let scrollPublisher = PassthroughSubject<String, Never>()
var markers: [Pen] { var markers: [Pen] {
@@ -32,6 +33,10 @@ public class Tool: NSObject, ObservableObject {
self.object = object self.object = object
} }
func selectTool(_ selection: ToolSelection) {
self.selection = selection
}
func load() { func load() {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { return } guard let self else { return }
@@ -116,6 +121,7 @@ public class Tool: NSObject, ObservableObject {
func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) { func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) {
guard let (resizedImage, dimension) = resizePhoto(of: image) else { return } guard let (resizedImage, dimension) = resizePhoto(of: image) else { return }
let photoItem = bookmarkPhoto(of: resizedImage, in: dimension, with: canvasID) let photoItem = bookmarkPhoto(of: resizedImage, in: dimension, with: canvasID)
isLoadingPhoto = false
withAnimation { withAnimation {
selectedPhotoItem = photoItem selectedPhotoItem = photoItem
} }
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation import Foundation
enum ToolSelection: Equatable { enum ToolSelection: Equatable {
case none case hand
case pen case pen
case photo case photo
} }
@@ -335,7 +335,7 @@ extension CanvasViewController {
let enablesDrawing: Bool let enablesDrawing: Bool
let enablesPhotoInsertion: Bool let enablesPhotoInsertion: Bool
switch selection { switch selection {
case .none: case .hand:
enablesScrolling = true enablesScrolling = true
enablesDrawing = false enablesDrawing = false
enablesPhotoInsertion = false enablesPhotoInsertion = false
@@ -362,7 +362,6 @@ extension CanvasViewController {
} }
func lockModeChanged(_ state: Bool) { func lockModeChanged(_ state: Bool) {
scrollView.isScrollEnabled = !state
scrollView.pinchGestureRecognizer?.isEnabled = !state scrollView.pinchGestureRecognizer?.isEnabled = !state
} }
} }
+8 -2
View File
@@ -32,7 +32,7 @@ struct MemoView: View {
.overlay(alignment: .bottomTrailing) { .overlay(alignment: .bottomTrailing) {
switch tool.selection { switch tool.selection {
case .pen: case .pen:
PenDock(tool: tool, canvas: canvas) PenDock(tool: tool, canvas: canvas, size: size)
.transition(.move(edge: .trailing)) .transition(.move(edge: .trailing))
case .photo: case .photo:
if let photoItem = tool.selectedPhotoItem { if let photoItem = tool.selectedPhotoItem {
@@ -46,7 +46,7 @@ struct MemoView: View {
.overlay(alignment: .bottomLeading) { .overlay(alignment: .bottomLeading) {
zoomControl zoomControl
} }
.disabled(textFieldState) .disabled(textFieldState || tool.isLoadingPhoto)
.overlay(alignment: .top) { .overlay(alignment: .top) {
Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history) Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history)
} }
@@ -61,6 +61,11 @@ struct MemoView: View {
EmptyView() EmptyView()
} }
} }
.overlay {
if tool.isLoadingPhoto {
loadingIndicator("Loading photo...")
}
}
} }
@ViewBuilder @ViewBuilder
@@ -96,6 +101,7 @@ struct MemoView: View {
.clipShape(.rect(cornerRadius: 8)) .clipShape(.rect(cornerRadius: 8))
.padding(10) .padding(10)
} }
.hoverEffect(.lift)
.transition(.move(edge: .bottom).combined(with: .blurReplace)) .transition(.move(edge: .bottom).combined(with: .blurReplace))
} }
} }
+33 -11
View File
@@ -11,6 +11,7 @@ struct PenDock: View {
@ObservedObject var tool: Tool @ObservedObject var tool: Tool
@ObservedObject var canvas: Canvas @ObservedObject var canvas: Canvas
let size: CGFloat
let width: CGFloat = 90 let width: CGFloat = 90
let height: CGFloat = 30 let height: CGFloat = 30
let factor: CGFloat = 0.9 let factor: CGFloat = 0.9
@@ -19,15 +20,20 @@ struct PenDock: View {
@State var opensColorPicker: Bool = false @State var opensColorPicker: Bool = false
var body: some View { var body: some View {
if !canvas.locksCanvas { ZStack(alignment: .bottomTrailing) {
VStack(alignment: .trailing) { if !canvas.locksCanvas {
penPropertyTool VStack(alignment: .trailing) {
penItemList penPropertyTool
penItemList
}
.fixedSize()
.frame(maxHeight: .infinity)
.padding(10)
.transition(.move(edge: .trailing).combined(with: .blurReplace))
} }
.fixedSize() lockButton
.frame(maxHeight: .infinity) .padding(10)
.padding(10) .transition(.move(edge: .trailing).combined(with: .blurReplace))
.transition(.move(edge: .trailing).combined(with: .blurReplace))
} }
} }
@@ -45,7 +51,6 @@ struct PenDock: View {
} }
} }
.padding(.vertical, 10) .padding(.vertical, 10)
.padding(.leading, 40)
.id(refreshScrollId) .id(refreshScrollId)
} }
.onReceive(tool.scrollPublisher) { id in .onReceive(tool.scrollPublisher) { id in
@@ -56,7 +61,7 @@ struct PenDock: View {
} }
} }
} }
.frame(maxHeight:( (height * factor + 10) * 6) + 20) .frame(maxHeight: ((height * factor + 10) * 6) + 20)
.fixedSize() .fixedSize()
.background(alignment: .trailing) { .background(alignment: .trailing) {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
@@ -66,7 +71,7 @@ struct PenDock: View {
.clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8))) .clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8)))
.overlay(alignment: .bottomLeading) { .overlay(alignment: .bottomLeading) {
newPenButton newPenButton
.offset(x: 60, y: 10) .offset(x: 15, y: 10)
} }
} }
@@ -235,6 +240,7 @@ struct PenDock: View {
.frame(width: size + 2, height: size + 2) .frame(width: size + 2, height: size + 2)
} }
} }
.hoverEffect(.lift)
.pickerStyle(.wheel) .pickerStyle(.wheel)
.frame(width: width * factor - 18, height: 35) .frame(width: width * factor - 18, height: 35)
.onChange(of: pen.thickness) { _, _ in .onChange(of: pen.thickness) { _, _ in
@@ -310,4 +316,20 @@ struct PenDock: View {
} }
} }
} }
var lockButton: some View {
Button {
withAnimation {
canvas.locksCanvas.toggle()
}
} label: {
Image(systemName: canvas.locksCanvas ? "lock.fill" : "lock.open.fill")
.contentShape(.circle)
.frame(width: size, height: size)
.background(.regularMaterial)
.clipShape(.rect(cornerRadius: 8))
}
.hoverEffect(.lift)
.contentTransition(.symbolEffect(.replace))
}
} }
+51 -31
View File
@@ -25,6 +25,8 @@ struct Toolbar: View {
@FocusState var textFieldState: Bool @FocusState var textFieldState: Bool
@Namespace var namespace
let size: CGFloat let size: CGFloat
init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) { init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) {
@@ -45,12 +47,13 @@ struct Toolbar: View {
} }
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
elementTool if !canvas.locksCanvas {
HStack(spacing: 5) { elementTool
}
Group {
if !canvas.locksCanvas { if !canvas.locksCanvas {
historyControl historyControl
} }
lockButton
} }
.frame(maxWidth: .infinity, alignment: .trailing) .frame(maxWidth: .infinity, alignment: .trailing)
} }
@@ -59,6 +62,7 @@ struct Toolbar: View {
.onChange(of: photosPickerItem) { oldValue, newValue in .onChange(of: photosPickerItem) { oldValue, newValue in
if newValue != nil { if newValue != nil {
Task { Task {
tool.isLoadingPhoto = true
let data = try? await newValue?.loadTransferable(type: Data.self) let data = try? await newValue?.loadTransferable(type: Data.self)
if let data, let image = UIImage(data: data) { if let data, let image = UIImage(data: data) {
tool.selectPhoto(image, for: canvas.canvasID) tool.selectPhoto(image, for: canvas.canvasID)
@@ -133,32 +137,68 @@ struct Toolbar: View {
HStack(spacing: 0) { HStack(spacing: 0) {
Button { Button {
withAnimation { withAnimation {
tool.selection = tool.selection == .pen ? .none : .pen tool.selectTool(.hand)
}
} label: {
Image(systemName: "hand.draw.fill")
.fontWeight(.heavy)
.contentShape(.circle)
.frame(width: size, height: size)
.foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor)
.clipShape(.rect(cornerRadius: 8))
}
.hoverEffect(.lift)
.background {
if tool.selection == .hand {
Color.accentColor
.clipShape(.rect(cornerRadius: 8))
.matchedGeometryEffect(id: "element.toolbar.bg", in: namespace)
}
}
Button {
withAnimation {
tool.selectTool(.pen)
} }
} label: { } label: {
Image(systemName: "pencil") Image(systemName: "pencil")
.fontWeight(.heavy) .fontWeight(.heavy)
.contentShape(.circle) .contentShape(.circle)
.frame(width: size, height: size) .frame(width: size, height: size)
.background(tool.selection == .pen ? Color.accentColor : Color.clear)
.foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor) .foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor)
.clipShape(.rect(cornerRadius: 8)) .clipShape(.rect(cornerRadius: 8))
} }
.hoverEffect(.lift) .hoverEffect(.lift)
.background {
if tool.selection == .pen {
Color.accentColor
.clipShape(.rect(cornerRadius: 8))
.matchedGeometryEffect(id: "element.toolbar.bg", in: namespace)
}
}
HStack(spacing: 0) { HStack(spacing: 0) {
Button { Button {
withAnimation { withAnimation {
tool.selection = tool.selection == .photo ? .none : .photo tool.selectTool(.photo)
} }
} label: { } label: {
Image(systemName: "photo") Image(systemName: "photo")
.contentShape(.circle) .contentShape(.circle)
.frame(width: size, height: size) .frame(width: size, height: size)
.background(tool.selection == .photo ? Color.accentColor : Color.clear)
.foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor) .foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor)
.clipShape(.rect(cornerRadius: 8)) .clipShape(.rect(cornerRadius: 8))
} }
.hoverEffect(.lift) .hoverEffect(.lift)
.background {
if tool.selection == .photo {
Color.accentColor
.clipShape(.rect(cornerRadius: 8))
.matchedGeometryEffect(id: "element.toolbar.bg", in: namespace)
}
if tool.selection != .photo {
Color.clear
.matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace)
}
}
if tool.selection == .photo { if tool.selection == .photo {
HStack(spacing: 0) { HStack(spacing: 0) {
Button { Button {
@@ -178,12 +218,15 @@ struct Toolbar: View {
} }
.hoverEffect(.lift) .hoverEffect(.lift)
} }
.matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace)
.transition(.blurReplace.animation(.easeIn(duration: 0.1)))
} }
} }
.background { .background {
if tool.selection == .photo { if tool.selection == .photo {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(Color.white.tertiary) .fill(Color.white.tertiary)
.transition(.move(edge: .leading).animation(.easeIn(duration: 0.1)))
} }
} }
} }
@@ -191,6 +234,7 @@ struct Toolbar: View {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(.regularMaterial) .fill(.regularMaterial)
} }
.transition(.move(edge: .top).combined(with: .blurReplace))
} }
var historyControl: some View { var historyControl: some View {
@@ -219,30 +263,6 @@ struct Toolbar: View {
.transition(.move(edge: .top).combined(with: .blurReplace)) .transition(.move(edge: .top).combined(with: .blurReplace))
} }
var lockButton: some View {
Button {
#warning("TODO: need to revisit toggale logic")
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 openCamera() { func openCamera() {
let status = AVCaptureDevice.authorizationStatus(for: .video) let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status { switch status {