From 6fe82fb2418aaea23171fcc5be7b923d9c7dfc7d Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 16:59:30 +0700 Subject: [PATCH 1/2] fix: replace .inspector() with custom panel to fix jittery animation Closes #243 --- TablePro/ContentView.swift | 54 ++++++++++--------- .../Views/Toolbar/TableProToolbarView.swift | 27 ++++++---- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/TablePro/ContentView.swift b/TablePro/ContentView.swift index 6e9d26dbd..5703cd738 100644 --- a/TablePro/ContentView.swift +++ b/TablePro/ContentView.swift @@ -257,33 +257,39 @@ struct ContentView: View { .navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 600) } detail: { // MARK: - Detail (Main workspace with optional right sidebar) - MainContentView( - connection: currentSession.connection, - payload: payload, - windowTitle: $windowTitle, - tables: sessionTablesBinding, - sidebarState: SharedSidebarState.forConnection(currentSession.connection.id), - pendingTruncates: sessionPendingTruncatesBinding, - pendingDeletes: sessionPendingDeletesBinding, - tableOperationOptions: sessionTableOperationOptionsBinding, - inspectorContext: $inspectorContext, - rightPanelState: rightPanelState, - tabManager: sessionState.tabManager, - changeManager: sessionState.changeManager, - filterStateManager: sessionState.filterStateManager, - toolbarState: sessionState.toolbarState, - coordinator: sessionState.coordinator - ) - .inspector(isPresented: Bindable(rightPanelState).isPresented) { - UnifiedRightPanelView( - state: rightPanelState, - inspectorContext: inspectorContext, + HStack(spacing: 0) { + MainContentView( connection: currentSession.connection, - tables: currentSession.tables + payload: payload, + windowTitle: $windowTitle, + tables: sessionTablesBinding, + sidebarState: SharedSidebarState.forConnection(currentSession.connection.id), + pendingTruncates: sessionPendingTruncatesBinding, + pendingDeletes: sessionPendingDeletesBinding, + tableOperationOptions: sessionTableOperationOptionsBinding, + inspectorContext: $inspectorContext, + rightPanelState: rightPanelState, + tabManager: sessionState.tabManager, + changeManager: sessionState.changeManager, + filterStateManager: sessionState.filterStateManager, + toolbarState: sessionState.toolbarState, + coordinator: sessionState.coordinator ) - .frame(minWidth: 280, maxWidth: 500) - .inspectorColumnWidth(min: 280, ideal: 320, max: 500) + .frame(maxWidth: .infinity) + + if rightPanelState.isPresented { + Divider() + UnifiedRightPanelView( + state: rightPanelState, + inspectorContext: inspectorContext, + connection: currentSession.connection, + tables: currentSession.tables + ) + .frame(width: 320) + .transition(.move(edge: .trailing)) + } } + .animation(.easeInOut(duration: 0.2), value: rightPanelState.isPresented) } .navigationTitle(windowTitle) .navigationSubtitle(currentSession.connection.name) diff --git a/TablePro/Views/Toolbar/TableProToolbarView.swift b/TablePro/Views/Toolbar/TableProToolbarView.swift index 3fda55aab..7fc4186cf 100644 --- a/TablePro/Views/Toolbar/TableProToolbarView.swift +++ b/TablePro/Views/Toolbar/TableProToolbarView.swift @@ -21,7 +21,7 @@ struct ToolbarPrincipalContent: View { var body: some View { HStack(spacing: 10) { if let tagId = state.tagId, - let tag = TagStorage.shared.tag(for: tagId) + let tag = TagStorage.shared.tag(for: tagId) { TagBadgeView(tag: tag) } @@ -130,14 +130,20 @@ struct TableProToolbar: ViewModifier { actions?.previewSQL() } label: { Label( - state.databaseType == .mongodb ? "Preview MQL" - : state.databaseType == .redis ? "Preview Commands" - : "Preview SQL", + state.databaseType == .mongodb + ? "Preview MQL" + : state.databaseType == .redis + ? "Preview Commands" + : "Preview SQL", systemImage: "eye") } - .help(state.databaseType == .mongodb ? "Preview MQL (⌘⇧P)" - : state.databaseType == .redis ? "Preview Commands (⌘⇧P)" - : "Preview SQL (⌘⇧P)") + .help( + state.databaseType == .mongodb + ? "Preview MQL (⌘⇧P)" + : state.databaseType == .redis + ? "Preview Commands (⌘⇧P)" + : "Preview SQL (⌘⇧P)" + ) .disabled(!state.hasPendingChanges || state.connectionState != .connected) } @@ -175,12 +181,15 @@ struct TableProToolbar: ViewModifier { Label("Import", systemImage: "square.and.arrow.down") } .help("Import Data (⌘⇧I)") - .disabled(state.connectionState != .connected || state.safeModeLevel.blocksAllWrites) + .disabled( + state.connectionState != .connected + || state.safeModeLevel.blocksAllWrites) } } } .popover(isPresented: $state.showSQLReviewPopover) { - SQLReviewPopover(statements: state.previewStatements, databaseType: state.databaseType) + SQLReviewPopover( + statements: state.previewStatements, databaseType: state.databaseType) } .onReceive(NotificationCenter.default.publisher(for: .openConnectionSwitcher)) { _ in showConnectionSwitcher = true From 156acb15aef21b32b9c9676591cd24d0087929cc Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 17:03:13 +0700 Subject: [PATCH 2/2] fix: replace .inspector() with custom resizable panel to fix jittery animation The .inspector() modifier uses an internal NSSplitViewController whose constraint-based animation becomes expensive at tight window widths. Replace with a lightweight HStack + transition panel that includes a drag-to-resize handle and persists the user's preferred width. Closes #243 --- TablePro/ContentView.swift | 3 +- TablePro/Models/UI/RightPanelState.swift | 15 +++++++ .../Views/Components/PanelResizeHandle.swift | 40 +++++++++++++++++++ .../Views/Toolbar/TableProToolbarView.swift | 27 +++++-------- 4 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 TablePro/Views/Components/PanelResizeHandle.swift diff --git a/TablePro/ContentView.swift b/TablePro/ContentView.swift index 5703cd738..14bbd13c0 100644 --- a/TablePro/ContentView.swift +++ b/TablePro/ContentView.swift @@ -278,6 +278,7 @@ struct ContentView: View { .frame(maxWidth: .infinity) if rightPanelState.isPresented { + PanelResizeHandle(panelWidth: Bindable(rightPanelState).panelWidth) Divider() UnifiedRightPanelView( state: rightPanelState, @@ -285,7 +286,7 @@ struct ContentView: View { connection: currentSession.connection, tables: currentSession.tables ) - .frame(width: 320) + .frame(width: rightPanelState.panelWidth) .transition(.move(edge: .trailing)) } } diff --git a/TablePro/Models/UI/RightPanelState.swift b/TablePro/Models/UI/RightPanelState.swift index d7afdf805..4c0888168 100644 --- a/TablePro/Models/UI/RightPanelState.swift +++ b/TablePro/Models/UI/RightPanelState.swift @@ -12,8 +12,13 @@ import os @MainActor @Observable final class RightPanelState { private static let isPresentedKey = "com.TablePro.rightPanel.isPresented" + private static let panelWidthKey = "com.TablePro.rightPanel.width" private static let isPresentedChangedNotification = Notification.Name("com.TablePro.rightPanel.isPresentedChanged") private var isSyncing = false + + static let minWidth: CGFloat = 280 + static let maxWidth: CGFloat = 500 + static let defaultWidth: CGFloat = 320 @ObservationIgnored private let _didTeardown = OSAllocatedUnfairLock(initialState: false) var isPresented: Bool { @@ -26,6 +31,14 @@ import os } } + var panelWidth: CGFloat { + didSet { + let clamped = min(max(panelWidth, Self.minWidth), Self.maxWidth) + if panelWidth != clamped { panelWidth = clamped } + UserDefaults.standard.set(Double(clamped), forKey: Self.panelWidthKey) + } + } + var activeTab: RightPanelTab = .details // Save closure — set by MainContentCommandActions, called by UnifiedRightPanelView @@ -43,6 +56,8 @@ import os init() { self.isPresented = UserDefaults.standard.bool(forKey: Self.isPresentedKey) + let savedWidth = UserDefaults.standard.double(forKey: Self.panelWidthKey) + self.panelWidth = savedWidth > 0 ? min(max(savedWidth, Self.minWidth), Self.maxWidth) : Self.defaultWidth NotificationCenter.default.addObserver( self, selector: #selector(handleIsPresentedChanged(_:)), diff --git a/TablePro/Views/Components/PanelResizeHandle.swift b/TablePro/Views/Components/PanelResizeHandle.swift new file mode 100644 index 000000000..6f8531c6b --- /dev/null +++ b/TablePro/Views/Components/PanelResizeHandle.swift @@ -0,0 +1,40 @@ +// +// PanelResizeHandle.swift +// TablePro +// +// Draggable resize handle for the right panel. +// + +import SwiftUI + +struct PanelResizeHandle: View { + @Binding var panelWidth: CGFloat + + @State private var isDragging = false + + var body: some View { + Rectangle() + .fill(Color.clear) + .frame(width: 5) + .contentShape(Rectangle()) + .onHover { hovering in + if hovering { + NSCursor.resizeLeftRight.push() + } else { + NSCursor.pop() + } + } + .gesture( + DragGesture(minimumDistance: 1) + .onChanged { value in + isDragging = true + // Dragging left increases panel width (handle is on the leading edge) + let newWidth = panelWidth - value.translation.width + panelWidth = min(max(newWidth, RightPanelState.minWidth), RightPanelState.maxWidth) + } + .onEnded { _ in + isDragging = false + } + ) + } +} diff --git a/TablePro/Views/Toolbar/TableProToolbarView.swift b/TablePro/Views/Toolbar/TableProToolbarView.swift index 7fc4186cf..3fda55aab 100644 --- a/TablePro/Views/Toolbar/TableProToolbarView.swift +++ b/TablePro/Views/Toolbar/TableProToolbarView.swift @@ -21,7 +21,7 @@ struct ToolbarPrincipalContent: View { var body: some View { HStack(spacing: 10) { if let tagId = state.tagId, - let tag = TagStorage.shared.tag(for: tagId) + let tag = TagStorage.shared.tag(for: tagId) { TagBadgeView(tag: tag) } @@ -130,20 +130,14 @@ struct TableProToolbar: ViewModifier { actions?.previewSQL() } label: { Label( - state.databaseType == .mongodb - ? "Preview MQL" - : state.databaseType == .redis - ? "Preview Commands" - : "Preview SQL", + state.databaseType == .mongodb ? "Preview MQL" + : state.databaseType == .redis ? "Preview Commands" + : "Preview SQL", systemImage: "eye") } - .help( - state.databaseType == .mongodb - ? "Preview MQL (⌘⇧P)" - : state.databaseType == .redis - ? "Preview Commands (⌘⇧P)" - : "Preview SQL (⌘⇧P)" - ) + .help(state.databaseType == .mongodb ? "Preview MQL (⌘⇧P)" + : state.databaseType == .redis ? "Preview Commands (⌘⇧P)" + : "Preview SQL (⌘⇧P)") .disabled(!state.hasPendingChanges || state.connectionState != .connected) } @@ -181,15 +175,12 @@ struct TableProToolbar: ViewModifier { Label("Import", systemImage: "square.and.arrow.down") } .help("Import Data (⌘⇧I)") - .disabled( - state.connectionState != .connected - || state.safeModeLevel.blocksAllWrites) + .disabled(state.connectionState != .connected || state.safeModeLevel.blocksAllWrites) } } } .popover(isPresented: $state.showSQLReviewPopover) { - SQLReviewPopover( - statements: state.previewStatements, databaseType: state.databaseType) + SQLReviewPopover(statements: state.previewStatements, databaseType: state.databaseType) } .onReceive(NotificationCenter.default.publisher(for: .openConnectionSwitcher)) { _ in showConnectionSwitcher = true