Skip to content

Commit 0adfd15

Browse files
committed
fix(inspector): prevent window overflow on inspector pane toggle
1 parent 354769a commit 0adfd15

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
@@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4343
- The Generate Token sheet focuses the Token Name field on first open. (#1093)
4444
- Double-clicking a CSV or TSV file when TablePro is closed opens the file directly. (#1443)
4545
- Opening a `.sql` file names the tab after the file instead of showing "SQL Query". (#1220)
46+
- 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.
4647

4748
## [0.45.0] - 2026-05-26
4849

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

@@ -186,6 +187,94 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
186187
inspectorHosting.rootView = AnyView(buildInspectorView())
187188
}
188189

190+
internal struct PaneMinimum {
191+
internal let minimumThickness: CGFloat
192+
internal let isCollapsed: Bool
193+
}
194+
195+
internal static func resolvedContentMinSize(
196+
base: NSSize,
197+
panes: [PaneMinimum],
198+
dividerThickness: CGFloat
199+
) -> NSSize {
200+
let visiblePanes = panes.filter { !$0.isCollapsed }
201+
let paneWidth = visiblePanes.reduce(CGFloat.zero) { partialResult, pane in
202+
partialResult + max(CGFloat.zero, pane.minimumThickness)
203+
}
204+
let dividerCount = max(visiblePanes.count - 1, 0)
205+
let resolvedWidth = max(base.width, paneWidth + (CGFloat(dividerCount) * dividerThickness))
206+
return NSSize(width: resolvedWidth, height: base.height)
207+
}
208+
209+
private func recomputeWindowMinimumSize(
210+
sidebarCollapsed: Bool? = nil,
211+
inspectorCollapsed: Bool? = nil
212+
) {
213+
guard let window = view.window else { return }
214+
215+
if baseWindowContentMinSize == nil {
216+
baseWindowContentMinSize = window.contentRect(forFrameRect: NSRect(origin: .zero, size: window.minSize)).size
217+
}
218+
guard let baseWindowContentMinSize else { return }
219+
220+
let resolvedMinSize = Self.resolvedContentMinSize(
221+
base: baseWindowContentMinSize,
222+
panes: [
223+
PaneMinimum(
224+
minimumThickness: sidebarSplitItem?.minimumThickness ?? .zero,
225+
isCollapsed: sidebarCollapsed ?? (sidebarSplitItem?.isCollapsed ?? true)
226+
),
227+
PaneMinimum(
228+
minimumThickness: detailSplitItem?.minimumThickness ?? .zero,
229+
isCollapsed: detailSplitItem?.isCollapsed ?? false
230+
),
231+
PaneMinimum(
232+
minimumThickness: inspectorSplitItem?.minimumThickness ?? .zero,
233+
isCollapsed: inspectorCollapsed ?? (inspectorSplitItem?.isCollapsed ?? true)
234+
)
235+
],
236+
dividerThickness: splitView.dividerThickness
237+
)
238+
239+
if window.contentMinSize != resolvedMinSize {
240+
window.contentMinSize = resolvedMinSize
241+
}
242+
243+
let currentContentSize = window.contentRect(forFrameRect: window.frame).size
244+
guard currentContentSize.width < resolvedMinSize.width || currentContentSize.height < resolvedMinSize.height else { return }
245+
window.setContentSize(NSSize(
246+
width: max(currentContentSize.width, resolvedMinSize.width),
247+
height: max(currentContentSize.height, resolvedMinSize.height)
248+
))
249+
}
250+
251+
private func setCollapsed(
252+
_ isCollapsed: Bool,
253+
for splitItem: NSSplitViewItem?,
254+
prepareWindowMinimumSize: (() -> Void)? = nil
255+
) {
256+
guard let splitItem else { return }
257+
258+
if splitItem.isCollapsed == isCollapsed {
259+
recomputeWindowMinimumSize()
260+
return
261+
}
262+
263+
prepareWindowMinimumSize?()
264+
265+
guard view.window?.isVisible == true else {
266+
splitItem.isCollapsed = isCollapsed
267+
recomputeWindowMinimumSize()
268+
return
269+
}
270+
271+
NSAnimationContext.runAnimationGroup { _ in
272+
splitItem.animator().isCollapsed = isCollapsed
273+
} completionHandler: { [weak self] in
274+
self?.recomputeWindowMinimumSize()
275+
}
276+
}
277+
189278
override func viewWillAppear() {
190279
super.viewWillAppear()
191280
guard let window = view.window else { return }
@@ -209,6 +298,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
209298
}
210299

211300
installObservers()
301+
recomputeWindowMinimumSize()
212302
}
213303

214304
override func viewDidDisappear() {
@@ -274,11 +364,7 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
274364
sessionState = nil
275365
currentSession = nil
276366
sidebarContainer.updateSidebarState(nil, windowState: nil)
277-
if view.window?.isVisible == true {
278-
sidebarSplitItem.animator().isCollapsed = true
279-
} else {
280-
sidebarSplitItem.isCollapsed = true
281-
}
367+
setCollapsed(true, for: sidebarSplitItem)
282368
}
283369
return
284370
}
@@ -306,10 +392,9 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
306392
}
307393

308394
let collapseSidebar = newSession.driver == nil
309-
if view.window?.isVisible == true {
310-
sidebarSplitItem.animator().isCollapsed = collapseSidebar
311-
} else {
312-
sidebarSplitItem.isCollapsed = collapseSidebar
395+
setCollapsed(collapseSidebar, for: sidebarSplitItem) { [weak self] in
396+
guard !collapseSidebar else { return }
397+
self?.recomputeWindowMinimumSize(sidebarCollapsed: false)
313398
}
314399
rebuildPanes()
315400
}
@@ -467,12 +552,14 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
467552

468553
func showInspector() {
469554
materializeInspectorIfNeeded()
470-
inspectorSplitItem?.animator().isCollapsed = false
555+
setCollapsed(false, for: inspectorSplitItem) { [weak self] in
556+
self?.recomputeWindowMinimumSize(inspectorCollapsed: false)
557+
}
471558
UserDefaults.standard.set(true, forKey: Self.inspectorPresentedKey)
472559
}
473560

474561
func hideInspector() {
475-
inspectorSplitItem?.animator().isCollapsed = true
562+
setCollapsed(true, for: inspectorSplitItem)
476563
UserDefaults.standard.set(false, forKey: Self.inspectorPresentedKey)
477564
}
478565

@@ -492,9 +579,11 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
492579

493580
if sidebarSplitItem?.isCollapsed == true {
494581
sidebarState.selectedSidebarTab = tab
495-
sidebarSplitItem?.animator().isCollapsed = false
582+
setCollapsed(false, for: sidebarSplitItem) { [weak self] in
583+
self?.recomputeWindowMinimumSize(sidebarCollapsed: false)
584+
}
496585
} else if sidebarState.selectedSidebarTab == tab {
497-
sidebarSplitItem?.animator().isCollapsed = true
586+
setCollapsed(true, for: sidebarSplitItem)
498587
} else {
499588
sidebarState.selectedSidebarTab = tab
500589
}
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)