From 6b5f3f47a33bc3aff0583773af27b7af0a79e06e Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 15 Mar 2026 13:56:57 +0700 Subject: [PATCH 1/4] feat: dynamic per-tab settings window sizing with animation --- TablePro/Views/Settings/SettingsView.swift | 22 +++++++++- .../Settings/SettingsWindowResizer.swift | 40 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 TablePro/Views/Settings/SettingsWindowResizer.swift diff --git a/TablePro/Views/Settings/SettingsView.swift b/TablePro/Views/Settings/SettingsView.swift index 4bb4f4078..77a348fc8 100644 --- a/TablePro/Views/Settings/SettingsView.swift +++ b/TablePro/Views/Settings/SettingsView.swift @@ -10,6 +10,21 @@ import SwiftUI /// Settings tab identifiers for programmatic navigation enum SettingsTab: String { case general, appearance, editor, dataGrid, keyboard, history, ai, plugins, sync, license + + var preferredSize: CGSize { + switch self { + case .general: CGSize(width: 450, height: 380) + case .appearance: CGSize(width: 720, height: 500) + case .editor: CGSize(width: 450, height: 300) + case .dataGrid: CGSize(width: 450, height: 380) + case .keyboard: CGSize(width: 500, height: 500) + case .history: CGSize(width: 450, height: 320) + case .ai: CGSize(width: 500, height: 520) + case .plugins: CGSize(width: 650, height: 500) + case .sync: CGSize(width: 450, height: 420) + case .license: CGSize(width: 450, height: 280) + } + } } /// Main settings view with tab-based navigation (macOS Settings style) @@ -18,6 +33,10 @@ struct SettingsView: View { @Environment(UpdaterBridge.self) var updaterBridge @AppStorage("selectedSettingsTab") private var selectedTab: String = SettingsTab.general.rawValue + private var currentTab: SettingsTab { + SettingsTab(rawValue: selectedTab) ?? .general + } + var body: some View { TabView(selection: $selectedTab) { GeneralSettingsView(settings: $settingsManager.general, updaterBridge: updaterBridge) @@ -81,7 +100,8 @@ struct SettingsView: View { } .tag(SettingsTab.license.rawValue) } - .frame(width: 720, height: 500) + .frame(width: currentTab.preferredSize.width, height: currentTab.preferredSize.height) + .background(SettingsWindowResizer(size: currentTab.preferredSize)) } } diff --git a/TablePro/Views/Settings/SettingsWindowResizer.swift b/TablePro/Views/Settings/SettingsWindowResizer.swift new file mode 100644 index 000000000..9e5a6f001 --- /dev/null +++ b/TablePro/Views/Settings/SettingsWindowResizer.swift @@ -0,0 +1,40 @@ +// +// SettingsWindowResizer.swift +// TablePro +// + +import AppKit +import SwiftUI + +/// Resizes the Settings window to match the selected tab's preferred size. +/// Uses AppKit's `NSWindow.setFrame(_:display:animate:)` for smooth transitions, +/// keeping the top-left corner pinned (standard macOS preferences behavior). +struct SettingsWindowResizer: NSViewRepresentable { + var size: CGSize + + func makeNSView(context: Context) -> NSView { + let view = _SettingsWindowSizeView() + return view + } + + func updateNSView(_ nsView: NSView, context: Context) { + guard let window = nsView.window else { return } + let contentSize = size + let newFrameSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: contentSize)).size + var frame = window.frame + // Pin top-left corner + frame.origin.y += frame.size.height - newFrameSize.height + frame.size = newFrameSize + let shouldAnimate = window.isVisible + window.setFrame(frame, display: true, animate: shouldAnimate) + window.minSize = newFrameSize + window.maxSize = newFrameSize + } +} + +private final class _SettingsWindowSizeView: NSView { + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + needsLayout = true + } +} From 50e36a31851fd77e937bb50081bd8cbb2afa0144 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 15 Mar 2026 14:01:20 +0700 Subject: [PATCH 2/4] fix: use fixed width with variable height for settings tabs --- TablePro/Views/Settings/SettingsView.swift | 28 ++++++++++--------- .../Settings/SettingsWindowResizer.swift | 18 +++++------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/TablePro/Views/Settings/SettingsView.swift b/TablePro/Views/Settings/SettingsView.swift index 77a348fc8..e95d46334 100644 --- a/TablePro/Views/Settings/SettingsView.swift +++ b/TablePro/Views/Settings/SettingsView.swift @@ -11,18 +11,20 @@ import SwiftUI enum SettingsTab: String { case general, appearance, editor, dataGrid, keyboard, history, ai, plugins, sync, license - var preferredSize: CGSize { + static let fixedWidth: CGFloat = 650 + + var preferredHeight: CGFloat { switch self { - case .general: CGSize(width: 450, height: 380) - case .appearance: CGSize(width: 720, height: 500) - case .editor: CGSize(width: 450, height: 300) - case .dataGrid: CGSize(width: 450, height: 380) - case .keyboard: CGSize(width: 500, height: 500) - case .history: CGSize(width: 450, height: 320) - case .ai: CGSize(width: 500, height: 520) - case .plugins: CGSize(width: 650, height: 500) - case .sync: CGSize(width: 450, height: 420) - case .license: CGSize(width: 450, height: 280) + case .general: 380 + case .appearance: 500 + case .editor: 300 + case .dataGrid: 380 + case .keyboard: 500 + case .history: 320 + case .ai: 520 + case .plugins: 500 + case .sync: 420 + case .license: 280 } } } @@ -100,8 +102,8 @@ struct SettingsView: View { } .tag(SettingsTab.license.rawValue) } - .frame(width: currentTab.preferredSize.width, height: currentTab.preferredSize.height) - .background(SettingsWindowResizer(size: currentTab.preferredSize)) + .frame(width: SettingsTab.fixedWidth, height: currentTab.preferredHeight) + .background(SettingsWindowResizer(size: CGSize(width: SettingsTab.fixedWidth, height: currentTab.preferredHeight))) } } diff --git a/TablePro/Views/Settings/SettingsWindowResizer.swift b/TablePro/Views/Settings/SettingsWindowResizer.swift index 9e5a6f001..565c3b0ac 100644 --- a/TablePro/Views/Settings/SettingsWindowResizer.swift +++ b/TablePro/Views/Settings/SettingsWindowResizer.swift @@ -6,29 +6,25 @@ import AppKit import SwiftUI -/// Resizes the Settings window to match the selected tab's preferred size. -/// Uses AppKit's `NSWindow.setFrame(_:display:animate:)` for smooth transitions, +/// Resizes the Settings window height to match the selected tab's preferred size. +/// Width stays fixed to keep all tab items visible. Uses AppKit's +/// `NSWindow.setFrame(_:display:animate:)` for smooth height transitions, /// keeping the top-left corner pinned (standard macOS preferences behavior). struct SettingsWindowResizer: NSViewRepresentable { var size: CGSize func makeNSView(context: Context) -> NSView { - let view = _SettingsWindowSizeView() - return view + _SettingsWindowSizeView() } func updateNSView(_ nsView: NSView, context: Context) { guard let window = nsView.window else { return } - let contentSize = size - let newFrameSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: contentSize)).size + let newFrameSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: size)).size + guard window.frame.size != newFrameSize else { return } var frame = window.frame - // Pin top-left corner frame.origin.y += frame.size.height - newFrameSize.height frame.size = newFrameSize - let shouldAnimate = window.isVisible - window.setFrame(frame, display: true, animate: shouldAnimate) - window.minSize = newFrameSize - window.maxSize = newFrameSize + window.setFrame(frame, display: true, animate: window.isVisible) } } From 59048f957215bb33e685f7632af7c7ad676d2e96 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 15 Mar 2026 14:02:47 +0700 Subject: [PATCH 3/4] fix: defer window resize to avoid reentrant layout --- TablePro/Views/Settings/SettingsWindowResizer.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/TablePro/Views/Settings/SettingsWindowResizer.swift b/TablePro/Views/Settings/SettingsWindowResizer.swift index 565c3b0ac..0fc2b22a1 100644 --- a/TablePro/Views/Settings/SettingsWindowResizer.swift +++ b/TablePro/Views/Settings/SettingsWindowResizer.swift @@ -21,10 +21,13 @@ struct SettingsWindowResizer: NSViewRepresentable { guard let window = nsView.window else { return } let newFrameSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: size)).size guard window.frame.size != newFrameSize else { return } - var frame = window.frame - frame.origin.y += frame.size.height - newFrameSize.height - frame.size = newFrameSize - window.setFrame(frame, display: true, animate: window.isVisible) + // Defer to next run loop tick to avoid reentrant layout during SwiftUI rendering + DispatchQueue.main.async { + var frame = window.frame + frame.origin.y += frame.size.height - newFrameSize.height + frame.size = newFrameSize + window.setFrame(frame, display: true, animate: window.isVisible) + } } } From 01203e965f256e1f2e997a2719a3779704a0fb62 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 15 Mar 2026 14:04:11 +0700 Subject: [PATCH 4/4] fix: increase settings width to 720 to fit all tab items --- TablePro/Views/Settings/SettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TablePro/Views/Settings/SettingsView.swift b/TablePro/Views/Settings/SettingsView.swift index e95d46334..bc07dd103 100644 --- a/TablePro/Views/Settings/SettingsView.swift +++ b/TablePro/Views/Settings/SettingsView.swift @@ -11,7 +11,7 @@ import SwiftUI enum SettingsTab: String { case general, appearance, editor, dataGrid, keyboard, history, ai, plugins, sync, license - static let fixedWidth: CGFloat = 650 + static let fixedWidth: CGFloat = 720 var preferredHeight: CGFloat { switch self {