@@ -1,4 +1,5 @@
1import SwiftUI
1import SwiftUI
2+import UniformTypeIdentifiers
3/// Computes the display width needed for a text string using the given font.
4/// Computes the display width needed for a text string using the given font.
4private func measureTextWidth(_ text: String, font: NSFont) -> CGFloat {
5private func measureTextWidth(_ text: String, font: NSFont) -> CGFloat {
@@ -224,6 +225,9 @@ struct LogHeaderView: View {
224 var onMoveColumn: ((String, String) -> Void)?
225 var onMoveColumn: ((String, String) -> Void)?
225 var onColumnFilter: ((_ column: String, _ value: JSONValue?, _ exclude: Bool) -> Void)?
226 var onColumnFilter: ((_ column: String, _ value: JSONValue?, _ exclude: Bool) -> Void)?
228+ @State private var draggedColumn: String?
229+ @State private var dropTargetColumn: String?
227 private static let maxFilterValues = 20
231 private static let maxFilterValues = 20
229 private func uniqueValues(for column: String) -> [JSONValue] {
233 private func uniqueValues(for column: String) -> [JSONValue] {
@@ -238,6 +242,15 @@ struct LogHeaderView: View {
238 return result.sorted()
242 return result.sorted()
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 {
251+ return fromIndex < toIndex ? .trailing : .leading
241 private func filterDisplayLabel(for value: JSONValue) -> String {
254 private func filterDisplayLabel(for value: JSONValue) -> String {
242 let str = value.displayString
255 let str = value.displayString
243 if str.isEmpty { return "(empty)" }
256 if str.isEmpty { return "(empty)" }
@@ -284,6 +297,28 @@ struct LogHeaderView: View {
286 .frame(width: columnWidths[column] ?? 120, alignment: .leading)
299 .frame(width: columnWidths[column] ?? 120, alignment: .leading)
301+ if dropTargetColumn == column && draggedColumn != nil && draggedColumn != column {
302+ Color.accentColor.opacity(0.15)
305+ .overlay(alignment: dropIndicatorAlignment(for: column)) {
306+ if dropTargetColumn == column && draggedColumn != nil && draggedColumn != column {
308+ .fill(Color.accentColor)
313+ draggedColumn = column
314+ return NSItemProvider(object: column as NSString)
316+ .onDrop(of: [UTType.plainText], delegate: ColumnDropDelegate(
318+ draggedColumn: $draggedColumn,
319+ dropTargetColumn: $dropTargetColumn,
320+ onMoveColumn: onMoveColumn
287 .contextMenu {
322 .contextMenu {
288 if let onMove = onMoveColumn {
323 if let onMove = onMoveColumn {
289 if column != columns.first {
324 if column != columns.first {
@@ -348,6 +383,50 @@ struct LogHeaderView: View {
386+// MARK: - ColumnDropDelegate
388+private struct ColumnDropDelegate: DropDelegate {
390+ @Binding var draggedColumn: String?
391+ @Binding var dropTargetColumn: String?
392+ var onMoveColumn: ((String, String) -> Void)?
394+ func dropEntered(info: DropInfo) {
395+ guard let dragged = draggedColumn, dragged != column else { return }
396+ withAnimation(.easeInOut(duration: 0.2)) {
397+ dropTargetColumn = column
401+ func dropExited(info: DropInfo) {
402+ withAnimation(.easeInOut(duration: 0.2)) {
403+ if dropTargetColumn == column {
404+ dropTargetColumn = nil
409+ func dropUpdated(info: DropInfo) -> DropProposal? {
410+ DropProposal(operation: .move)
413+ func performDrop(info: DropInfo) -> Bool {
414+ guard let dragged = draggedColumn, dragged != column else {
416+ dropTargetColumn = nil
419+ onMoveColumn?(dragged, column)
421+ dropTargetColumn = nil
425+ func validateDrop(info: DropInfo) -> Bool {
426+ draggedColumn != nil && draggedColumn != column
351// MARK: - LogRowView
430// MARK: - LogRowView
353struct LogRowView: View {
432struct LogRowView: View {