@@ -104,8 +104,6 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
104104 var tabStripObservationIsArmed = false
105105 var tabStripObservedManager : ObjectIdentifier ?
106106
107- private var chromeState : ChromeState = . unapplied
108-
109107 // MARK: - Panel Layout State
110108
111109 /// One name for the window's split view, because one `NSSplitView` can only carry one.
@@ -253,6 +251,10 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
253251 navigationSidebar. railController. host = self
254252 navigationSidebar. railController. onLayoutChange = { [ weak self] _ in
255253 self ? . navigationSidebar. applyRailWidth ( animated: false )
254+ /// The row size is a setting, so the rail's own width changes under a sidebar already
255+ /// narrowed to it. Reapplying the clamp is what moves both thicknesses onto the new
256+ /// allowance rather than clipping the rail against the old one.
257+ self ? . reapplySidebarClampIfNarrowed ( )
256258 self ? . recomputeWindowMinSize ( )
257259 }
258260 sidebarSplitItem = NSSplitViewItem ( sidebarWithViewController: navigationSidebar)
@@ -935,6 +937,11 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
935937 navigationSidebar. setRailVisible ( visible, animated: view. window != nil ) { [ weak self] in
936938 self ? . recomputeWindowMinSize ( )
937939 }
940+ /// The rail appearing or going is a chrome change of its own, and it happens without the
941+ /// selected connection's phase moving: a sibling opening or closing is enough. Without this
942+ /// a window whose connection is down keeps a fully collapsed sidebar when the rail arrives,
943+ /// or an empty clamped column after it leaves.
944+ applyPaneChrome ( )
938945 }
939946
940947 func activateWorkspace( offsetBy offset: Int ) {
@@ -943,8 +950,25 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
943950
944951 // MARK: - Sidebar
945952
953+ /// Whether the object browser is off screen, which is the question every caller is really
954+ /// asking: the toolbar's segment, the Show/Hide Sidebar title and the reveal actions. A sidebar
955+ /// narrowed to the workspace rail is an open split item with no object browser in it, so the
956+ /// item's own flag is not the answer on its own.
946957 var isSidebarCollapsed : Bool {
947- sidebarSplitItem? . isCollapsed ?? true
958+ guard sidebarChromeMode. showsObjectBrowser else { return true }
959+ return sidebarSplitItem? . isCollapsed ?? true
960+ }
961+
962+ var isSidebarUserCollapsible : Bool {
963+ sidebarSplitItem? . canCollapse ?? false
964+ }
965+
966+ var sidebarThicknessRange : ( min: CGFloat , max: CGFloat ) {
967+ ( sidebarSplitItem? . minimumThickness ?? 0 , sidebarSplitItem? . maximumThickness ?? 0 )
968+ }
969+
970+ var railAllowance : CGFloat {
971+ navigationSidebar? . railAllowance ?? 0
948972 }
949973
950974 /// Every collapse route reaches AppKit's own `toggleSidebar(_:)`: the View menu sends the
@@ -957,13 +981,15 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
957981 }
958982
959983 func focusSidebarSearch( ) {
984+ guard sidebarChromeMode. showsObjectBrowser else { return }
960985 if sidebarSplitItem? . isCollapsed == true {
961986 sidebarSplitItem? . animator ( ) . isCollapsed = false
962987 }
963988 navigationSidebar. objectBrowser. focusSearchField ( )
964989 }
965990
966991 func presentDatabaseFilter( ) {
992+ guard sidebarChromeMode. showsObjectBrowser else { return }
967993 guard let connectionId = currentSession? . connection. id else { return }
968994 if sidebarSplitItem? . isCollapsed == true {
969995 sidebarSplitItem? . isCollapsed = false
@@ -981,7 +1007,11 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
9811007 state. databaseFilterSelected = [ ]
9821008 }
9831009
1010+ /// Refused while the sidebar is narrowed to the workspace rail. The item is open, so the
1011+ /// collapse branch below would read it as showing and collapse it, taking the rail and every
1012+ /// route to the window's other connections with it.
9841013 func setSidebarTab( _ tab: SidebarTab ) {
1014+ guard sidebarChromeMode. showsObjectBrowser else { return }
9851015 guard let connectionId = currentSession? . connection. id else { return }
9861016 let sidebarState = SharedSidebarState . forConnection ( connectionId)
9871017
@@ -1062,8 +1092,12 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
10621092 )
10631093 }
10641094
1095+ /// Inert while the sidebar is clamped to the rail. Seven other call sites reach
1096+ /// `recomputeWindowMinSize`, and any of them writing the object browser's minimum back over the
1097+ /// clamp would leave a minimum above the maximum.
10651098 private func applySidebarMinimumThickness( ) {
10661099 guard let sidebarSplitItem else { return }
1100+ guard appliedSidebarMode ?? . revealed == . revealed else { return }
10671101 let resolved = Self . resolveSidebarMinimumThickness (
10681102 railAllowance: navigationSidebar? . railAllowance ?? 0
10691103 )
@@ -1108,52 +1142,99 @@ internal final class MainSplitViewController: NSSplitViewController, InspectorVi
11081142
11091143 // MARK: - Pane Chrome
11101144
1111- private enum ChromeState {
1112- case unapplied
1113- case hidden
1114- case revealed
1115- }
1116-
11171145 private var userPaneLayout : ChromePaneLayout ?
11181146
1147+ /// What this window's sidebar is currently showing, and `nil` before the first application.
1148+ /// The resolver decides the mode; this records which one has been put on screen.
1149+ private var appliedSidebarMode : SidebarChromeMode ?
1150+
1151+ internal var sidebarChromeMode : SidebarChromeMode {
1152+ ConnectionWindowPaneResolver . sidebarChromeMode (
1153+ for: currentPane,
1154+ hasRail: navigationSidebar? . isRailVisible ?? false
1155+ )
1156+ }
1157+
11191158 /// A split item's collapse state is written into the autosave record, which is how the
11201159 /// inspector remembers being hidden. Collapsing the sidebar for a phase the user did not
11211160 /// choose would persist that as their layout and lose the width they set, so autosaving is
1122- /// switched off for the whole span the chrome is hidden and switched back on to restore it.
1161+ /// switched off for the whole span the chrome is not revealed and switched back on to restore
1162+ /// it. The same is true of a clamp, which writes its narrow width into the record the way a
1163+ /// collapse writes the collapsed flag.
11231164 func applyPaneChrome( ) {
1124- if ConnectionWindowPaneResolver . hidesChrome ( for: currentPane) {
1125- hideWindowChrome ( )
1126- } else {
1127- revealWindowChrome ( )
1128- }
1165+ applySidebarChromeMode ( sidebarChromeMode)
11291166 applyTabStripVisibility ( )
11301167 toolbarOwner? . managedToolbar. validateVisibleItems ( )
11311168 recomputeWindowMinSize ( )
11321169 }
11331170
1134- private func hideWindowChrome( ) {
1135- guard chromeState != . hidden else { return }
1136- chromeState = . hidden
1171+ private func applySidebarChromeMode( _ mode: SidebarChromeMode ) {
1172+ guard appliedSidebarMode != mode else { return }
1173+ let previous = appliedSidebarMode
1174+ appliedSidebarMode = mode
1175+
1176+ guard mode != . revealed else {
1177+ revealWindowChrome ( )
1178+ return
1179+ }
1180+
1181+ /// Captured on the way out of `revealed` and never again, because the geometry a
1182+ /// `railOnly` to `hidden` step would see is the clamp, not the width the user chose.
1183+ if previous == nil || previous == . revealed {
1184+ resignFirstResponderInsideChrome ( )
1185+ splitView. autosaveName = nil
1186+ userPaneLayout = ChromePaneLayout (
1187+ isSidebarCollapsed: sidebarSplitItem. isCollapsed,
1188+ isInspectorCollapsed: inspectorSplitItem. isCollapsed
1189+ )
1190+ }
11371191
1138- resignFirstResponderInsideChrome ( )
1139- splitView. autosaveName = nil
1140- userPaneLayout = ChromePaneLayout (
1141- isSidebarCollapsed: sidebarSplitItem. isCollapsed,
1142- isInspectorCollapsed: inspectorSplitItem. isCollapsed
1143- )
1144- sidebarSplitItem. isCollapsed = true
11451192 inspectorSplitItem. isCollapsed = true
1193+ switch mode {
1194+ case . railOnly:
1195+ sidebarSplitItem. isCollapsed = false
1196+ clampSidebarToRail ( )
1197+ case . hidden:
1198+ releaseSidebarClamp ( )
1199+ sidebarSplitItem. isCollapsed = true
1200+ case . revealed:
1201+ break
1202+ }
11461203 view. window? . recalculateKeyViewLoop ( )
11471204 }
11481205
1149- /// Autosaving is off while the chrome is hidden, so the record still holds what the user had.
1150- /// AppKit will not re-apply it though: assigning an autosave name to a split view that has
1206+ /// Narrowed rather than collapsed, so the rail stays on screen while the object browser it
1207+ /// shares a split item with goes. Measured: clamping and later releasing returns the item to
1208+ /// the width the user set, but a `setPosition` while the clamp holds discards it, which is why
1209+ /// nothing else may write the sidebar's thickness for the span.
1210+ private func clampSidebarToRail( ) {
1211+ let allowance = navigationSidebar? . railAllowance ?? 0
1212+ sidebarSplitItem. minimumThickness = allowance
1213+ sidebarSplitItem. maximumThickness = allowance
1214+ /// A clamp is not a lock. AppKit still collapses a collapsible item on a divider
1215+ /// double-click or a drag to the edge, which no menu or toolbar validation sees, and the
1216+ /// mode is already applied so nothing would open it again.
1217+ sidebarSplitItem. canCollapse = false
1218+ }
1219+
1220+ internal func reapplySidebarClampIfNarrowed( ) {
1221+ guard appliedSidebarMode == . railOnly else { return }
1222+ clampSidebarToRail ( )
1223+ }
1224+
1225+ private func releaseSidebarClamp( ) {
1226+ sidebarSplitItem. canCollapse = true
1227+ sidebarSplitItem. maximumThickness = Self . sidebarMaxThickness
1228+ applySidebarMinimumThickness ( )
1229+ }
1230+
1231+ /// Autosaving is off while the chrome is not revealed, so the record still holds what the user
1232+ /// had. AppKit will not re-apply it though: assigning an autosave name to a split view that has
11511233 /// already laid out restores nothing. The state captured on the way in is therefore what gives
11521234 /// the panes back. Forcing the sidebar open here instead reopened a sidebar the user had
11531235 /// deliberately hidden, every time a connection dropped and came back.
11541236 private func revealWindowChrome( ) {
1155- guard chromeState != . revealed else { return }
1156- chromeState = . revealed
1237+ releaseSidebarClamp ( )
11571238
11581239 /// Only a reveal that follows a hide has something to put back. A first reveal is a window
11591240 /// opening on a live connection, where the panes are already where the user's autosaved
0 commit comments