feat: add reordering and deleting

This commit is contained in:
dscyrescotti
2024-05-18 01:25:36 +07:00
parent 4701eac3ba
commit 821b34e158
5 changed files with 137 additions and 36 deletions

View File

@@ -10,6 +10,7 @@
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14202BF79C73009BFE5F /* ToolObject.swift */; };
EC0D14242BF79C98009BFE5F /* MemolaModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14222BF79C98009BFE5F /* MemolaModel.xcdatamodeld */; };
EC0D14262BF7A8C9009BFE5F /* PenObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14252BF7A8C9009BFE5F /* PenObject.swift */; };
EC0D14282BF7BF20009BFE5F /* ContextMenuableViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.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 */; };
@@ -86,6 +87,7 @@
EC0D14202BF79C73009BFE5F /* ToolObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolObject.swift; sourceTree = "<group>"; };
EC0D14232BF79C98009BFE5F /* MemolaModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MemolaModel.xcdatamodel; sourceTree = "<group>"; };
EC0D14252BF7A8C9009BFE5F /* PenObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenObject.swift; sourceTree = "<group>"; };
EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuableViewModifier.swift; sourceTree = "<group>"; };
EC3565512BEFC65F00A4E0BF /* NSManagedObjectContext++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext++.swift"; sourceTree = "<group>"; };
EC3565532BEFC6AD00A4E0BF /* View++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View++.swift"; sourceTree = "<group>"; };
EC3565552BEFC7B300A4E0BF /* NSManagedObject++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject++.swift"; sourceTree = "<group>"; };
@@ -232,6 +234,7 @@
isa = PBXGroup;
children = (
EC50500C2BF6674400B4D86E /* DraggableViewModifier.swift */,
EC0D14272BF7BF20009BFE5F /* ContextMenuableViewModifier.swift */,
);
path = ViewModifiers;
sourceTree = "<group>";
@@ -734,6 +737,7 @@
ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */,
EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */,
ECA738A02BE601E400A4542E /* ViewPortVertex.swift in Sources */,
EC0D14282BF7BF20009BFE5F /* ContextMenuableViewModifier.swift in Sources */,
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,

View File

@@ -15,6 +15,9 @@ public class Tool: NSObject, ObservableObject {
@Published var pens: [Pen] = []
@Published var selectedPen: Pen?
@Published var draggedPen: Pen?
@Published var isReordering: Bool = false
@Published var isShaking: Bool = false
@Published var shakingId: UUID = UUID()
init(object: ToolObject) {
self.object = object
@@ -45,6 +48,9 @@ public class Tool: NSObject, ObservableObject {
func unselectPen(_ pen: Pen) {
pen.isSelected = false
withAnimation {
selectedPen = nil
}
}
func addPen(_ pen: Pen) {
@@ -56,4 +62,15 @@ public class Tool: NSObject, ObservableObject {
object.pens.add(_pen)
}
}
func removePen(_ pen: Pen) {
guard let index = pens.firstIndex(where: { $0 === pen }) else { return }
let deletedPen = withAnimation {
pens.remove(at: index)
}
if let _pen = deletedPen.object {
_pen.tool = nil
object.pens.remove(_pen)
}
}
}

View File

@@ -0,0 +1,29 @@
//
// ContextMenuableViewModifier.swift
// Memola
//
// Created by Dscyre Scotti on 5/17/24.
//
import SwiftUI
import Foundation
struct ContextMenuableViewModifier<MenuContent: View>: ViewModifier {
let condition: Bool
let menuItems: () -> MenuContent
@ViewBuilder
func body(content: Content) -> some View {
if condition {
content.contextMenu(menuItems: menuItems)
} else {
content
}
}
}
public extension View {
func contextMenu<MenuContent: View>(if condition: Bool, @ViewBuilder menuItems: @escaping () -> MenuContent) -> some View {
modifier(ContextMenuableViewModifier(condition: condition, menuItems: menuItems))
}
}

View File

@@ -27,6 +27,7 @@ struct PenDropDelegate: DropDelegate {
tool.pens.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
tool.objectWillChange.send()
}
tool.shakingId = UUID()
withPersistence(\.viewContext) { context in
for (index, pen) in tool.pens.enumerated() {
pen.object?.orderIndex = Int16(index)

View File

@@ -17,17 +17,42 @@ struct PenToolView: View {
var body: some View {
VStack(alignment: .trailing, spacing: 0) {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(tool.pens) { pen in
penView(pen)
if tool.isReordering {
LazyVStack(spacing: 0) {
ForEach(tool.pens) { pen in
if pen.strokeStyle == .marker {
penView(pen)
.offset(y: tool.isShaking ? 1.5 : -1.5)
} else {
penView(pen)
}
}
}
.padding(.vertical, 5)
.padding(.leading, 40)
.onAppear {
withAnimation(.easeInOut.repeatForever().speed(5)) {
tool.isShaking.toggle()
}
}
.id(tool.shakingId)
} else {
LazyVStack(spacing: 0) {
ForEach(tool.pens) { pen in
penView(pen)
}
}
.padding(.vertical, 5)
.padding(.leading, 40)
}
.padding(.vertical, 5)
.padding(.leading, 40)
}
VStack(spacing: 0) {
Divider()
newPenButton
if tool.isReordering {
reorderCancelButton
} else {
newPenButton
}
}
.frame(width: width * factor - 20)
}
@@ -58,43 +83,40 @@ struct PenToolView: View {
.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) {
.onTapGesture {
if tool.selectedPen === pen {
tool.unselectPen(pen)
} else {
tool.selectPen(pen)
}
}
.disabled(tool.isReordering)
.contextMenu(if: pen.strokeStyle != .eraser && !tool.isReordering) {
Button {
tool.isReordering = true
} label: {
Label("Rearrange", systemImage: "arrow.up.arrow.down.circle")
}
Button(role: .destructive) {
tool.removePen(pen)
} label: {
Label("Delete", systemImage: "trash")
}
}
.onDrag(if: pen.strokeStyle != .eraser && tool.isReordering) {
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))
penPreview(pen)
.contentShape(.dragPreview, .rect(cornerRadius: 10))
}
.onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool))
.onTapGesture {
if tool.selectedPen === pen {
withAnimation {
tool.unselectPen(pen)
}
} else {
withAnimation {
tool.selectPen(pen)
}
}
}
.offset(x: tool.selectedPen === pen ? 0 : 28)
}
var newPenButton: some View {
Button(action: {
Button {
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
pen.color = [Color.red, Color.blue, Color.green, Color.black, Color.orange].randomElement()!.components
pen.isSelected = true
@@ -102,12 +124,40 @@ struct PenToolView: View {
pen.orderIndex = Int16(tool.pens.count)
let _pen = Pen(object: pen)
tool.addPen(_pen)
}) {
Image(systemName: "plus")
.font(.title3)
} label: {
Image(systemName: "pencil.tip.crop.circle.badge.plus")
.font(.title2)
.contentShape(.circle)
}
.hoverEffect(.lift)
.padding(10)
}
var reorderCancelButton: some View {
Button {
tool.isReordering = false
} label: {
Image(systemName: "xmark.circle")
.font(.title2)
.contentShape(.circle)
}
.hoverEffect(.lift)
.padding(10)
}
func penPreview(_ 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)
}
}