listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

commit fa4040496b251aad12f0225f636f00c929540597
parent 47e27704ad6399b4a21778ae7860d7433cd55ff8
Author: Michael Camilleri <[email protected]>
Date:   Wed, 25 Feb 2026 13:25:21 +0900

Fix cursor during drag reordering in macOS

Co-Authored-By: Codex GPT 5.3 <[email protected]>

Diffstat:
MListless/Extensions/TaskListView+Logic.swift | 5++---
MListlessMac/Views/TaskListView.swift | 88+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
2 files changed, 59 insertions(+), 34 deletions(-)

diff --git a/Listless/Extensions/TaskListView+Logic.swift b/Listless/Extensions/TaskListView+Logic.swift @@ -414,9 +414,8 @@ extension TaskListView { } } - func handleDrop(items: [String]) -> Bool { - guard let droppedUUIDString = items.first, - let droppedUUID = UUID(uuidString: droppedUUIDString), + func commitCurrentDrag() -> Bool { + guard let droppedUUID = draggedTaskID, let order = visualOrder, let finalIndex = order.firstIndex(of: droppedUUID) else { diff --git a/ListlessMac/Views/TaskListView.swift b/ListlessMac/Views/TaskListView.swift @@ -1,4 +1,5 @@ import SwiftUI +import UniformTypeIdentifiers struct TaskListView: View { enum FocusField: Hashable { @@ -181,37 +182,37 @@ struct TaskListView: View { Color.clear .frame(maxHeight: .infinity) .layoutPriority(1) - .dropDestination( - for: String.self, action: { _, _ in false }, - isTargeted: { isTargeted in - if isTargeted { - updateVisualOrder(insertBefore: task.id) - } - }) + .onDrop( + of: [UTType.text], + delegate: TaskReorderDropDelegate( + onTargeted: { updateVisualOrder(insertBefore: task.id) }, + onPerform: { commitCurrentDrag() } + ) + ) // Middle 2/3 - insert based on direction Color.clear .frame(maxHeight: .infinity) .layoutPriority(4) - .dropDestination( - for: String.self, action: { _, _ in false }, - isTargeted: { isTargeted in - if isTargeted { - updateVisualOrderSmart(relativeTo: task.id) - } - }) + .onDrop( + of: [UTType.text], + delegate: TaskReorderDropDelegate( + onTargeted: { updateVisualOrderSmart(relativeTo: task.id) }, + onPerform: { commitCurrentDrag() } + ) + ) // Bottom 1/6 - insert AFTER Color.clear .frame(maxHeight: .infinity) .layoutPriority(1) - .dropDestination( - for: String.self, action: { _, _ in false }, - isTargeted: { isTargeted in - if isTargeted { - updateVisualOrder(insertAfter: task.id) - } - }) + .onDrop( + of: [UTType.text], + delegate: TaskReorderDropDelegate( + onTargeted: { updateVisualOrder(insertAfter: task.id) }, + onPerform: { commitCurrentDrag() } + ) + ) } } } @@ -221,13 +222,13 @@ struct TaskListView: View { if !activeTasks.isEmpty && draggedTaskID != nil { Color.clear .frame(height: 44) - .dropDestination( - for: String.self, action: { _, _ in false }, - isTargeted: { isTargeted in - if isTargeted { - updateVisualOrder(insertAtEnd: true) - } - }) + .onDrop( + of: [UTType.text], + delegate: TaskReorderDropDelegate( + onTargeted: { updateVisualOrder(insertAtEnd: true) }, + onPerform: { commitCurrentDrag() } + ) + ) } ForEach(completedTasks) { task in @@ -245,9 +246,13 @@ struct TaskListView: View { } } .frame(maxWidth: .infinity, alignment: .topLeading) - .dropDestination(for: String.self) { items, location in - handleDrop(items: items) - } + .onDrop( + of: [UTType.text], + delegate: TaskReorderDropDelegate( + onTargeted: {}, + onPerform: { commitCurrentDrag() } + ) + ) } .contentShape(Rectangle()) .onTapGesture { @@ -333,3 +338,24 @@ struct TaskListView: View { } } } + +private struct TaskReorderDropDelegate: DropDelegate { + let onTargeted: () -> Void + let onPerform: () -> Bool + + func validateDrop(info: DropInfo) -> Bool { + true + } + + func dropEntered(info: DropInfo) { + onTargeted() + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + return DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + onPerform() + } +}