Files
Memola/Memola/Features/Memo/PenDock/PenDock.swift
T
2024-06-10 23:11:11 +07:00

316 lines
11 KiB
Swift

//
// PenDock.swift
// Memola
//
// Created by Dscyre Scotti on 5/4/24.
//
import SwiftUI
struct PenDock: View {
@ObservedObject var tool: Tool
@ObservedObject var canvas: Canvas
let width: CGFloat = 90
let height: CGFloat = 30
let factor: CGFloat = 0.9
@State var refreshScrollId: UUID = UUID()
@State var opensColorPicker: Bool = false
var body: some View {
if !canvas.locksCanvas {
VStack(alignment: .trailing) {
penPropertyTool
penItemList
}
.fixedSize()
.frame(maxHeight: .infinity)
.padding(10)
.transition(.move(edge: .trailing).combined(with: .blurReplace))
}
}
var penItemList: some View {
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(tool.pens) { pen in
penItemRow(pen)
.id(pen.id)
.scrollTransition { content, phase in
content
.scaleEffect(phase.isIdentity ? 1 : 0.04, anchor: .trailing)
}
}
}
.padding(.vertical, 10)
.padding(.leading, 40)
.id(refreshScrollId)
}
.onReceive(tool.scrollPublisher) { id in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
withAnimation {
proxy.scrollTo(id)
}
}
}
}
.frame(maxHeight:( (height * factor + 10) * 6) + 20)
.fixedSize()
.background(alignment: .trailing) {
RoundedRectangle(cornerRadius: 8)
.fill(.regularMaterial)
.frame(width: width * factor - 18)
}
.clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8)))
.overlay(alignment: .bottomLeading) {
newPenButton
.offset(x: 60, y: 10)
}
}
func penItemRow(_ pen: Pen) -> some View {
ZStack {
penShadow(pen)
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.rgba))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: width * factor, height: height * factor)
.padding(.vertical, 5)
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
.onTapGesture {
if tool.selectedPen === pen {
tool.unselectPen(pen)
} else {
tool.selectPen(pen)
}
}
.padding(.leading, 10)
.contextMenu(if: pen.strokeStyle != .eraser) {
ControlGroup {
Button {
tool.selectPen(pen)
} label: {
Label(
title: { Text("Select") },
icon: { Image(systemName: "pencil.tip.crop.circle") }
)
}
Button {
let originalPen = pen
let pen = PenObject.createObject(\.viewContext, penStyle: originalPen.style)
pen.color = originalPen.rgba
pen.thickness = originalPen.thickness
pen.isSelected = true
pen.tool = tool.object
let _pen = Pen(object: pen)
tool.duplicatePen(_pen, of: originalPen)
} label: {
Label(
title: { Text("Duplicate") },
icon: { Image(systemName: "plus.square.on.square") }
)
}
Button(role: .destructive) {
tool.removePen(pen)
} label: {
Label(
title: { Text("Remove") },
icon: { Image(systemName: "trash") }
)
}
.disabled(tool.markers.count <= 1)
}
.controlGroupStyle(.menu)
} preview: {
penPreview(pen)
.drawingGroup()
.contentShape(.contextMenuPreview, .rect(cornerRadius: 10))
}
.onDrag(if: pen.strokeStyle != .eraser) {
tool.draggedPen = pen
return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider()
} preview: {
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)
}
@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: {
let hsba = pen.color.hsba
let baseColor = Color(hue: hsba.hue, saturation: hsba.saturation, brightness: hsba.brightness)
GeometryReader { proxy in
HStack(spacing: 0) {
baseColor
.frame(width: proxy.size.width / 2)
Image("transparent-grid-square")
.resizable()
.scaleEffect(3)
.aspectRatio(contentMode: .fill)
.opacity(1 - hsba.alpha)
.frame(width: proxy.size.width / 2)
.clipped()
}
}
.background(baseColor)
.clipShape(.rect(cornerRadius: 8))
.frame(height: 25)
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.gray, lineWidth: 0.4)
}
.padding(0.2)
}
.buttonStyle(.plain)
.hoverEffect(.lift)
.popover(isPresented: $opensColorPicker) {
let color = Binding(
get: { pen.color },
set: {
pen.color = $0
tool.objectWillChange.send()
}
)
ColorPicker(color: color)
.presentationCompactAdaptation(.popover)
.onDisappear {
withPersistence(\.viewContext) { context in
try context.saveIfNeeded()
}
}
}
}
@ViewBuilder
func penThicknessPicker(_ 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 = 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)
}
}
.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 {
Button {
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
var selectedPen = tool.selectedPen
selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last)
if let color = selectedPen?.rgba {
pen.color = color
}
pen.isSelected = true
pen.tool = tool.object
pen.orderIndex = Int16(tool.pens.count)
let _pen = Pen(object: pen)
tool.addPen(_pen)
} label: {
Image(systemName: "plus.circle.fill")
.font(.title2)
.padding(1)
.contentShape(.circle)
.background {
Circle()
.fill(.white)
}
}
.foregroundStyle(.green)
.hoverEffect(.lift)
}
func penPreview(_ pen: Pen) -> some View {
ZStack {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color.rgba(from: pen.rgba))
}
Image(pen.style.icon.base)
.resizable()
}
.frame(width: width * factor, height: height * factor)
.padding(.vertical, 5)
.padding(.leading, 10)
}
func penShadow(_ pen: Pen) -> some View {
ZStack {
Group {
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
}
Image(pen.style.icon.base)
.resizable()
.renderingMode(.template)
}
.foregroundStyle(.black.opacity(0.2))
.blur(radius: 3)
if let tip = pen.style.icon.tip {
Image(tip)
.resizable()
.renderingMode(.template)
.foregroundStyle(Color(red: pen.rgba[0], green: pen.rgba[1], blue: pen.rgba[2]))
.blur(radius: 0.5)
}
}
}
}