feat: redesign pen tool dock

This commit is contained in:
dscyrescotti
2024-05-17 00:04:54 +07:00
parent 5748fe685d
commit 3204328e5e
27 changed files with 321 additions and 103 deletions
@@ -11,17 +11,22 @@ import Foundation
class Tool: NSObject, ObservableObject {
@Published var pens: [Pen]
@Published var selectedPen: Pen?
@Published var draggedPen: Pen?
override init() {
pens = [
Pen(for: .marker),
Pen(for: .eraser)
Pen(for: .eraser),
Pen(for: .marker)
]
super.init()
selectedPen = pens.first
selectedPen = pens[1]
}
func changePen(_ pen: Pen) {
selectedPen = pen
}
func addPen(_ pen: Pen) {
pens.append(pen)
}
}
+3
View File
@@ -7,13 +7,16 @@
import SwiftUI
import Foundation
import UniformTypeIdentifiers
class Pen: NSObject, ObservableObject, Identifiable {
let id: String
@Published var style: any PenStyle
@Published var color: [CGFloat]
@Published var thickness: CGFloat
init(style: any PenStyle, color: [CGFloat], thickness: CGFloat) {
self.id = UUID().uuidString
self.style = style
self.color = color
self.thickness = thickness
@@ -0,0 +1,30 @@
//
// DraggableViewModifier.swift
// Memola
//
// Created by Dscyre Scotti on 5/16/24.
//
import SwiftUI
import Foundation
struct DraggableViewModifier<Preview: View>: ViewModifier {
let condition: Bool
let data: () -> NSItemProvider
let preview: () -> Preview
@ViewBuilder
func body(content: Content) -> some View {
if condition {
content.onDrag(data, preview: preview)
} else {
content
}
}
}
public extension View {
func onDrag<Preview: View>(if condition: Bool, data: @escaping () -> NSItemProvider, @ViewBuilder preview: @escaping () -> Preview) -> some View {
modifier(DraggableViewModifier(condition: condition, data: data, preview: preview))
}
}
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>UILaunchScreen</key>
<dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>UISupportedInterfaceOrientations~iphone</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
</dict>
</plist>
@@ -26,19 +26,19 @@ struct MemoView: View {
var body: some View {
CanvasView()
.ignoresSafeArea()
.overlay(alignment: .bottomTrailing) {
PenToolView()
.padding()
}
.overlay(alignment: .topTrailing) {
historyTool
.padding()
VStack(alignment: .trailing, spacing: 20) {
historyTool
PenToolView()
}
.padding()
}
.overlay(alignment: .topLeading) {
Button {
closeMemo()
} label: {
Image(systemName: "xmark")
.contentShape(.circle)
.padding(15)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
@@ -68,6 +68,7 @@ struct MemoView: View {
history.historyPublisher.send(.undo)
} label: {
Image(systemName: "arrow.uturn.backward.circle")
.contentShape(.circle)
}
.hoverEffect(.lift)
.disabled(history.undoDisabled)
@@ -75,6 +76,7 @@ struct MemoView: View {
history.historyPublisher.send(.redo)
} label: {
Image(systemName: "arrow.uturn.forward.circle")
.contentShape(.circle)
}
.hoverEffect(.lift)
.disabled(history.redoDisabled)
@@ -0,0 +1,32 @@
//
// PenDropDelegate.swift
// Memola
//
// Created by Dscyre Scotti on 5/16/24.
//
import SwiftUI
import Foundation
struct PenDropDelegate: DropDelegate {
let id: String
@ObservedObject var tool: Tool
func performDrop(info: DropInfo) -> Bool {
tool.draggedPen = nil
return true
}
func dropEntered(info: DropInfo) {
guard let draggedPen = tool.draggedPen else { return }
if draggedPen.id != id {
let fromIndex = tool.pens.firstIndex(where: { $0.id == draggedPen.id })!
let toIndex = tool.pens.firstIndex(where: { $0.id == id })!
guard tool.pens[toIndex].strokeStyle != .eraser else { return }
withAnimation {
tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
tool.objectWillChange.send()
}
}
}
}
@@ -0,0 +1,109 @@
//
// PenToolView.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
struct PenToolView: View {
@EnvironmentObject var tool: Tool
let width: CGFloat = 80
let height: CGFloat = 30
let factor: CGFloat = 1.22
var body: some View {
VStack(alignment: .trailing, spacing: 0) {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(tool.pens) { pen in
penView(pen)
}
}
.padding(.vertical, 5)
.padding(.leading, 40)
}
VStack(spacing: 0) {
Divider()
newPenButton
}
.frame(width: width * factor - 20)
}
.frame(maxHeight: (height * factor + 10) * 8)
.fixedSize()
.background {
HStack(spacing: 0) {
Spacer(minLength: 70)
RoundedRectangle(cornerRadius: 20)
.fill(.regularMaterial)
}
}
.clipShape(.rect(cornerRadii: .init(bottomTrailing: 20, topTrailing: 20)))
}
@ViewBuilder
func penView(_ pen: Pen) -> some View {
ZStack {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.color))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: width * factor, height: height * factor)
.padding(.vertical, 5)
.padding(.leading, 10)
.clipShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
.onDrag(if: pen.strokeStyle != .eraser) {
tool.draggedPen = pen
return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider()
} preview: {
ZStack {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.color))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: width * factor, height: height * factor)
.padding([.vertical, .leading], 10)
.contentShape(.dragPreview, .rect(cornerRadius: 10))
}
.onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool))
.onTapGesture {
if tool.selectedPen === pen {
withAnimation {
tool.selectedPen = nil
}
} else {
withAnimation {
tool.changePen(pen)
}
}
}
.offset(x: tool.selectedPen === pen ? 0 : 28)
}
var newPenButton: some View {
Button(action: {
let pen = Pen(for: .marker)
pen.color = [Color.red, Color.blue, Color.green, Color.black, Color.orange].randomElement()!.components
tool.addPen(pen)
}) {
Image(systemName: "plus")
.font(.title3)
.contentShape(.circle)
}
.hoverEffect(.lift)
.padding(10)
}
}
-82
View File
@@ -1,82 +0,0 @@
//
// PenToolView.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
struct PenToolView: View {
@EnvironmentObject var tool: Tool
var body: some View {
VStack {
if let pen = tool.selectedPen {
let thicknessBounds = pen.style.thinkness
let thickness = Binding {
max(pen.thickness, pen.style.thinkness.min)
} set: { newValue in
tool.selectedPen?.thickness = newValue
}
let color = Binding {
Color.rgba(from: pen.color)
} set: { newValue in
tool.selectedPen?.color = newValue.components
tool.objectWillChange.send()
}
HStack {
ColorPicker("", selection: color)
.frame(width: 40, height: 40)
Slider(value: thickness, in: thicknessBounds.min...thicknessBounds.max)
.frame(width: 180, height: 40)
}
}
HStack {
ForEach(tool.pens) { pen in
penView(pen)
.overlay(alignment: .bottom) {
if tool.selectedPen === pen {
Circle()
.frame(width: 5, height: 5)
.offset(y: 7.5)
.foregroundStyle(Color.rgba(from: pen.color))
}
}
}
}
.padding(15)
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 20))
}
}
@ViewBuilder
func penView(_ pen: Pen) -> some View {
Button {
if tool.selectedPen === pen {
tool.selectedPen = nil
} else {
tool.changePen(pen)
}
} label: {
ZStack {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.color))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: 30, height: 65)
.drawingGroup()
.hoverEffect(.lift)
}
.buttonStyle(.plain)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "bullet-base.png",
"filename" : "marker-base.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bullet-base@2x.png",
"filename" : "marker-base@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bullet-base@3x.png",
"filename" : "marker-base@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "bullet-tip.png",
"filename" : "marker-tip.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bullet-tip@2x.png",
"filename" : "marker-tip@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bullet-tip@3x.png",
"filename" : "marker-tip@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB