git.iko.soy/parseable-macos
on master help sign in
Add drag-and-drop column reordering to table headers (#22)

Column headers in LogHeaderView can now be dragged to reorder them directly in the table, using macOS native drag-and-drop. Visual feedback includes an accent-colored highlight on the drop target and an insertion indicator line on the appropriate edge (leading when dragging left, trailing when dragging right). The existing context menu Move Left/Right and column manager popover reordering remain unchanged. https://claude.ai/code/session_01MrSExir3akjhN3TGHMdvwQ Co-authored-by: Claude <noreply@anthropic.com>

commit5ef360aaeddec240e6ed7dd743622e95f68d9865 authoriko <mail@iko.soy> dateSun 15 Feb 2026, 18:39 (8 months ago) parent04345a8
1 file changed+79
ParseableViewer/Views/LogTableView.swift+79
ParseableViewer/Views/LogTableView.swift+79
@@ -1,4 +1,5 @@
11import SwiftUI
2+import UniformTypeIdentifiers
23
34/// Computes the display width needed for a text string using the given font.
45private func measureTextWidth(_ text: String, font: NSFont) -> CGFloat {
@@ -224,6 +225,9 @@ struct LogHeaderView: View {
224225 var onMoveColumn: ((String, String) -> Void)?
225226 var onColumnFilter: ((_ column: String, _ value: JSONValue?, _ exclude: Bool) -> Void)?
226227
228+ @State private var draggedColumn: String?
229+ @State private var dropTargetColumn: String?
230+
227231 private static let maxFilterValues = 20
228232
229233 private func uniqueValues(for column: String) -> [JSONValue] {
@@ -238,6 +242,15 @@ struct LogHeaderView: View {
238242 return result.sorted()
239243 }
240244
245+ private func dropIndicatorAlignment(for targetColumn: String) -> Alignment {
246+ guard let dragged = draggedColumn,
247+ let fromIndex = columns.firstIndex(of: dragged),
248+ let toIndex = columns.firstIndex(of: targetColumn) else {
249+ return .leading
250+ }
251+ return fromIndex < toIndex ? .trailing : .leading
252+ }
253+
241254 private func filterDisplayLabel(for value: JSONValue) -> String {
242255 let str = value.displayString
243256 if str.isEmpty { return "(empty)" }
@@ -284,6 +297,28 @@ struct LogHeaderView: View {
284297 )
285298 }
286299 .frame(width: columnWidths[column] ?? 120, alignment: .leading)
300+ .background {
301+ if dropTargetColumn == column && draggedColumn != nil && draggedColumn != column {
302+ Color.accentColor.opacity(0.15)
303+ }
304+ }
305+ .overlay(alignment: dropIndicatorAlignment(for: column)) {
306+ if dropTargetColumn == column && draggedColumn != nil && draggedColumn != column {
307+ Rectangle()
308+ .fill(Color.accentColor)
309+ .frame(width: 2)
310+ }
311+ }
312+ .onDrag {
313+ draggedColumn = column
314+ return NSItemProvider(object: column as NSString)
315+ }
316+ .onDrop(of: [UTType.plainText], delegate: ColumnDropDelegate(
317+ column: column,
318+ draggedColumn: $draggedColumn,
319+ dropTargetColumn: $dropTargetColumn,
320+ onMoveColumn: onMoveColumn
321+ ))
287322 .contextMenu {
288323 if let onMove = onMoveColumn {
289324 if column != columns.first {
@@ -348,6 +383,50 @@ struct LogHeaderView: View {
348383 }
349384}
350385
386+// MARK: - ColumnDropDelegate
387+
388+private struct ColumnDropDelegate: DropDelegate {
389+ let column: String
390+ @Binding var draggedColumn: String?
391+ @Binding var dropTargetColumn: String?
392+ var onMoveColumn: ((String, String) -> Void)?
393+
394+ func dropEntered(info: DropInfo) {
395+ guard let dragged = draggedColumn, dragged != column else { return }
396+ withAnimation(.easeInOut(duration: 0.2)) {
397+ dropTargetColumn = column
398+ }
399+ }
400+
401+ func dropExited(info: DropInfo) {
402+ withAnimation(.easeInOut(duration: 0.2)) {
403+ if dropTargetColumn == column {
404+ dropTargetColumn = nil
405+ }
406+ }
407+ }
408+
409+ func dropUpdated(info: DropInfo) -> DropProposal? {
410+ DropProposal(operation: .move)
411+ }
412+
413+ func performDrop(info: DropInfo) -> Bool {
414+ guard let dragged = draggedColumn, dragged != column else {
415+ draggedColumn = nil
416+ dropTargetColumn = nil
417+ return false
418+ }
419+ onMoveColumn?(dragged, column)
420+ draggedColumn = nil
421+ dropTargetColumn = nil
422+ return true
423+ }
424+
425+ func validateDrop(info: DropInfo) -> Bool {
426+ draggedColumn != nil && draggedColumn != column
427+ }
428+}
429+
351430// MARK: - LogRowView
352431
353432struct LogRowView: View {