Skip to content

Commit 0cc0c3a

Browse files
committed
fix(inspector): prevent window overflow on inspector pane toggle
1 parent 97c7f11 commit 0cc0c3a

3 files changed

Lines changed: 159 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- Installing or updating a plugin right after updating TablePro now refetches the current plugin list first, so it no longer fails against a stale cached list (the error a restart used to clear). (#1380)
2828
- Pressing Esc to close the Raw SQL filter suggestions, or to clear a search field, no longer also exits fullscreen. (#1403)
2929
- Connecting an OAuth-capable MCP client like Claude Code with an invalid or expired token now shows a clear error instead of a confusing "Invalid OAuth error response". (#1409)
30+
- Toggling the right inspector in a narrow editor window now updates the window minimum width from the visible split panes, so the inspector no longer squeezes content or overflows.
3031

3132
## [0.44.0] - 2026-05-23
3233

TablePro/Core/Services/Infrastructure/MainSplitViewController.swift

Lines changed: 102 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
4040
private var detailHosting: NSHostingController<AnyView>!
4141
private var inspectorHosting: NSHostingController<AnyView>!
4242
private var hasMaterializedInspector = false
43+
private var baseWindowContentMinSize: NSSize?
4344

4445
// MARK: - Toolbar
4546

@@ -169,6 +170,94 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
169170
inspectorHosting.rootView = AnyView(buildInspectorView())
170171
}
171172

173+
internal struct PaneMinimum {
174+
internal let minimumThickness: CGFloat
175+
internal let isCollapsed: Bool
176+
}
177+
178+
internal static func resolvedContentMinSize(
179+
base: NSSize,
180+
panes: [PaneMinimum],
181+
dividerThickness: CGFloat
182+
) -> NSSize {
183+
let visiblePanes = panes.filter { !$0.isCollapsed }
184+
let paneWidth = visiblePanes.reduce(CGFloat.zero) { partialResult, pane in
185+
partialResult + max(CGFloat.zero, pane.minimumThickness)
186+
}
187+
let dividerCount = max(visiblePanes.count - 1, 0)
188+
let resolvedWidth = max(base.width, paneWidth + (CGFloat(dividerCount) * dividerThickness))
189+
return NSSize(width: resolvedWidth, height: base.height)
190+
}
191+
192+
private func recomputeWindowMinimumSize(
193+
sidebarCollapsed: Bool? = nil,
194+
inspectorCollapsed: Bool? = nil
195+
) {
196+
guard let window = view.window else { return }
197+
198+
if baseWindowContentMinSize == nil {
199+
baseWindowContentMinSize = window.contentRect(forFrameRect: NSRect(origin: .zero, size: window.minSize)).size
200+
}
201+
guard let baseWindowContentMinSize else { return }
202+
203+
let resolvedMinSize = Self.resolvedContentMinSize(
204+
base: baseWindowContentMinSize,
205+
panes: [
206+
PaneMinimum(
207+
minimumThickness: sidebarSplitItem?.minimumThickness ?? .zero,
208+
isCollapsed: sidebarCollapsed ?? (sidebarSplitItem?.isCollapsed ?? true)
209+
),
210+
PaneMinimum(
211+
minimumThickness: detailSplitItem?.minimumThickness ?? .zero,
212+
isCollapsed: detailSplitItem?.isCollapsed ?? false
213+
),
214+
PaneMinimum(
215+
minimumThickness: inspectorSplitItem?.minimumThickness ?? .zero,
216+
isCollapsed: inspectorCollapsed ?? (inspectorSplitItem?.isCollapsed ?? true)
217+
)
218+
],
219+
dividerThickness: splitView.dividerThickness
220+
)
221+
222+
if window.contentMinSize != resolvedMinSize {
223+
window.contentMinSize = resolvedMinSize
224+
}
225+
226+
let currentContentSize = window.contentRect(forFrameRect: window.frame).size
227+
guard currentContentSize.width < resolvedMinSize.width || currentContentSize.height < resolvedMinSize.height else { return }
228+
window.setContentSize(NSSize(
229+
width: max(currentContentSize.width, resolvedMinSize.width),
230+
height: max(currentContentSize.height, resolvedMinSize.height)
231+
))
232+
}
233+
234+
private func setCollapsed(
235+
_ isCollapsed: Bool,
236+
for splitItem: NSSplitViewItem?,
237+
prepareWindowMinimumSize: (() -> Void)? = nil
238+
) {
239+
guard let splitItem else { return }
240+
241+
if splitItem.isCollapsed == isCollapsed {
242+
recomputeWindowMinimumSize()
243+
return
244+
}
245+
246+
prepareWindowMinimumSize?()
247+
248+
guard view.window?.isVisible == true else {
249+
splitItem.isCollapsed = isCollapsed
250+
recomputeWindowMinimumSize()
251+
return
252+
}
253+
254+
NSAnimationContext.runAnimationGroup { _ in
255+
splitItem.animator().isCollapsed = isCollapsed
256+
} completionHandler: { [weak self] in
257+
self?.recomputeWindowMinimumSize()
258+
}
259+
}
260+
172261
override func viewWillAppear() {
173262
super.viewWillAppear()
174263
guard let window = view.window else { return }
@@ -192,6 +281,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
192281
}
193282

194283
installObservers()
284+
recomputeWindowMinimumSize()
195285
}
196286

197287
override func viewDidDisappear() {
@@ -257,11 +347,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
257347
sessionState = nil
258348
currentSession = nil
259349
sidebarContainer.updateSidebarState(nil, windowState: nil)
260-
if view.window?.isVisible == true {
261-
sidebarSplitItem.animator().isCollapsed = true
262-
} else {
263-
sidebarSplitItem.isCollapsed = true
264-
}
350+
setCollapsed(true, for: sidebarSplitItem)
265351
}
266352
return
267353
}
@@ -289,10 +375,9 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
289375
}
290376

291377
let collapseSidebar = newSession.driver == nil
292-
if view.window?.isVisible == true {
293-
sidebarSplitItem.animator().isCollapsed = collapseSidebar
294-
} else {
295-
sidebarSplitItem.isCollapsed = collapseSidebar
378+
setCollapsed(collapseSidebar, for: sidebarSplitItem) { [weak self] in
379+
guard !collapseSidebar else { return }
380+
self?.recomputeWindowMinimumSize(sidebarCollapsed: false)
296381
}
297382
rebuildPanes()
298383
}
@@ -450,12 +535,14 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
450535

451536
func showInspector() {
452537
materializeInspectorIfNeeded()
453-
inspectorSplitItem?.animator().isCollapsed = false
538+
setCollapsed(false, for: inspectorSplitItem) { [weak self] in
539+
self?.recomputeWindowMinimumSize(inspectorCollapsed: false)
540+
}
454541
UserDefaults.standard.set(true, forKey: Self.inspectorPresentedKey)
455542
}
456543

457544
func hideInspector() {
458-
inspectorSplitItem?.animator().isCollapsed = true
545+
setCollapsed(true, for: inspectorSplitItem)
459546
UserDefaults.standard.set(false, forKey: Self.inspectorPresentedKey)
460547
}
461548

@@ -475,9 +562,11 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
475562

476563
if sidebarSplitItem?.isCollapsed == true {
477564
sidebarState.selectedSidebarTab = tab
478-
sidebarSplitItem?.animator().isCollapsed = false
565+
setCollapsed(false, for: sidebarSplitItem) { [weak self] in
566+
self?.recomputeWindowMinimumSize(sidebarCollapsed: false)
567+
}
479568
} else if sidebarState.selectedSidebarTab == tab {
480-
sidebarSplitItem?.animator().isCollapsed = true
569+
setCollapsed(true, for: sidebarSplitItem)
481570
} else {
482571
sidebarState.selectedSidebarTab = tab
483572
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import AppKit
2+
import Testing
3+
4+
@testable import TablePro
5+
6+
@Suite("MainSplitViewController window minimum size")
7+
@MainActor
8+
struct MainSplitViewControllerWindowMinimumSizeTests {
9+
@Test("Uses all visible pane minimums when the inspector is shown")
10+
func includesVisibleInspectorPane() {
11+
let size = MainSplitViewController.resolvedContentMinSize(
12+
base: NSSize(width: 720, height: 448),
13+
panes: [
14+
.init(minimumThickness: 280, isCollapsed: false),
15+
.init(minimumThickness: 400, isCollapsed: false),
16+
.init(minimumThickness: 270, isCollapsed: false)
17+
],
18+
dividerThickness: 2
19+
)
20+
21+
#expect(size.width == 954)
22+
#expect(size.height == 448)
23+
}
24+
25+
@Test("Keeps the base width floor when the inspector is hidden")
26+
func keepsBaseWidthWhenInspectorHidden() {
27+
let size = MainSplitViewController.resolvedContentMinSize(
28+
base: NSSize(width: 720, height: 448),
29+
panes: [
30+
.init(minimumThickness: 280, isCollapsed: false),
31+
.init(minimumThickness: 400, isCollapsed: false),
32+
.init(minimumThickness: 270, isCollapsed: true)
33+
],
34+
dividerThickness: 2
35+
)
36+
37+
#expect(size.width == 720)
38+
#expect(size.height == 448)
39+
}
40+
41+
@Test("Relaxes to the base width when only detail and inspector remain")
42+
func keepsBaseWidthWithSidebarCollapsed() {
43+
let size = MainSplitViewController.resolvedContentMinSize(
44+
base: NSSize(width: 720, height: 448),
45+
panes: [
46+
.init(minimumThickness: 280, isCollapsed: true),
47+
.init(minimumThickness: 400, isCollapsed: false),
48+
.init(minimumThickness: 270, isCollapsed: false)
49+
],
50+
dividerThickness: 2
51+
)
52+
53+
#expect(size.width == 720)
54+
#expect(size.height == 448)
55+
}
56+
}

0 commit comments

Comments
 (0)