From 034f157febd109d153a83f9d3af24aed13650467 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 10 Mar 2026 00:41:00 -0700 Subject: [PATCH] Keep browser omnibar visible across pane zoom --- Sources/Panels/BrowserPanel.swift | 70 ++++++++++++++++++++++++++++++- Sources/Workspace.swift | 47 ++++++++++++++++++++- 2 files changed, 114 insertions(+), 3 deletions(-) diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index aaad9f23..d9c06dc6 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -1720,7 +1720,13 @@ final class BrowserPanel: Panel, ObservableObject { let inWindow: Bool let area: CGFloat } + private struct PortalHostLock { + let hostId: ObjectIdentifier + let paneId: UUID + } private var activePortalHostLease: PortalHostLease? + private var pendingDistinctPortalHostReplacementPaneId: UUID? + private var lockedPortalHost: PortalHostLock? private var webViewCancellables = Set() private var navigationDelegate: BrowserNavigationDelegate? private var uiDelegate: BrowserUIDelegate? @@ -1773,6 +1779,22 @@ final class BrowserPanel: Panel, ObservableObject { lease.inWindow && lease.area > portalHostAreaThreshold } + func preparePortalHostReplacementForNextDistinctClaim( + inPane paneId: PaneID, + reason: String + ) { + pendingDistinctPortalHostReplacementPaneId = paneId.id + if lockedPortalHost?.paneId == paneId.id { + lockedPortalHost = nil + } +#if DEBUG + dlog( + "browser.portal.host.rearm panel=\(id.uuidString.prefix(5)) " + + "reason=\(reason) pane=\(paneId.id.uuidString.prefix(5))" + ) +#endif + } + func claimPortalHost( hostId: ObjectIdentifier, paneId: PaneID, @@ -1788,6 +1810,11 @@ final class BrowserPanel: Panel, ObservableObject { ) if let current = activePortalHostLease { + if let lock = lockedPortalHost, + (lock.hostId != current.hostId || lock.paneId != current.paneId) { + lockedPortalHost = nil + } + if current.hostId == hostId { activePortalHostLease = next return true @@ -1795,12 +1822,47 @@ final class BrowserPanel: Panel, ObservableObject { let currentUsable = Self.portalHostIsUsable(current) let nextUsable = Self.portalHostIsUsable(next) + let isSamePaneReplacement = current.paneId == paneId.id + let shouldForceDistinctReplacement = + isSamePaneReplacement && + pendingDistinctPortalHostReplacementPaneId == paneId.id && + inWindow + if shouldForceDistinctReplacement { +#if DEBUG + dlog( + "browser.portal.host.claim panel=\(id.uuidString.prefix(5)) " + + "reason=\(reason) host=\(hostId) pane=\(paneId.id.uuidString.prefix(5)) " + + "inWin=\(inWindow ? 1 : 0) size=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " + + "replacingHost=\(current.hostId) replacingPane=\(current.paneId.uuidString.prefix(5)) " + + "replacingInWin=\(current.inWindow ? 1 : 0) replacingArea=\(String(format: "%.1f", current.area)) " + + "forced=1" + ) +#endif + activePortalHostLease = next + pendingDistinctPortalHostReplacementPaneId = nil + lockedPortalHost = PortalHostLock(hostId: hostId, paneId: paneId.id) + return true + } + + let lockBlocksSamePaneReplacement = + isSamePaneReplacement && + currentUsable && + lockedPortalHost?.hostId == current.hostId && + lockedPortalHost?.paneId == current.paneId let shouldReplace = current.paneId != paneId.id || !currentUsable || - (nextUsable && next.area > (current.area * Self.portalHostReplacementAreaGainRatio)) + ( + !lockBlocksSamePaneReplacement && + nextUsable && + next.area > (current.area * Self.portalHostReplacementAreaGainRatio) + ) if shouldReplace { + if lockedPortalHost?.hostId == current.hostId && + lockedPortalHost?.paneId == current.paneId { + lockedPortalHost = nil + } #if DEBUG dlog( "browser.portal.host.claim panel=\(id.uuidString.prefix(5)) " + @@ -1820,7 +1882,8 @@ final class BrowserPanel: Panel, ObservableObject { "reason=\(reason) host=\(hostId) pane=\(paneId.id.uuidString.prefix(5)) " + "inWin=\(inWindow ? 1 : 0) size=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " + "ownerHost=\(current.hostId) ownerPane=\(current.paneId.uuidString.prefix(5)) " + - "ownerInWin=\(current.inWindow ? 1 : 0) ownerArea=\(String(format: "%.1f", current.area))" + "ownerInWin=\(current.inWindow ? 1 : 0) ownerArea=\(String(format: "%.1f", current.area)) " + + "locked=\(lockBlocksSamePaneReplacement ? 1 : 0)" ) #endif return false @@ -1842,6 +1905,9 @@ final class BrowserPanel: Panel, ObservableObject { func releasePortalHostIfOwned(hostId: ObjectIdentifier, reason: String) -> Bool { guard let current = activePortalHostLease, current.hostId == hostId else { return false } activePortalHostLease = nil + if lockedPortalHost?.hostId == hostId { + lockedPortalHost = nil + } #if DEBUG dlog( "browser.portal.host.release panel=\(id.uuidString.prefix(5)) " + diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 9be189c3..1d8e569e 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -3217,11 +3217,19 @@ final class Workspace: Identifiable, ObservableObject { @discardableResult func toggleSplitZoom(panelId: UUID) -> Bool { + let wasSplitZoomed = bonsplitController.isSplitZoomed guard let paneId = paneId(forPanelId: panelId) else { return false } guard bonsplitController.togglePaneZoom(inPane: paneId) else { return false } focusPanel(panelId) - if browserPanel(for: panelId) != nil { + if let browserPanel = browserPanel(for: panelId) { + browserPanel.preparePortalHostReplacementForNextDistinctClaim( + inPane: paneId, + reason: "workspace.toggleSplitZoom" + ) scheduleBrowserPortalReconcileAfterSplitZoom(panelId: panelId, remainingPasses: 4) + if wasSplitZoomed && !bonsplitController.isSplitZoomed { + scheduleBrowserSplitZoomExitFocusReassert(panelId: panelId, remainingPasses: 4) + } } return true } @@ -3525,6 +3533,43 @@ final class Workspace: Identifiable, ObservableObject { } } + // Browser panes can briefly keep the portal-hosted WKWebView visible while Bonsplit is + // still rebuilding the unzoomed pane host. Reassert pane/tab selection after layout settles + // so the SwiftUI chrome does not remain hidden until another browser focus command runs. + private func scheduleBrowserSplitZoomExitFocusReassert(panelId: UUID, remainingPasses: Int) { + guard remainingPasses > 0 else { return } + DispatchQueue.main.async { [weak self] in + guard let self, self.browserPanel(for: panelId) != nil else { return } + guard let paneId = self.paneId(forPanelId: panelId), + let tabId = self.surfaceIdFromPanelId(panelId) else { return } + + let selectionConverged = + self.bonsplitController.focusedPaneId == paneId && + self.bonsplitController.selectedTab(inPane: paneId)?.id == tabId + let anchorReady: Bool = { + guard let browserPanel = self.browserPanel(for: panelId) else { return false } + let anchorView = browserPanel.portalAnchorView + return + anchorView.window != nil && + anchorView.superview != nil && + anchorView.bounds.width > 1 && + anchorView.bounds.height > 1 + }() + + if !selectionConverged { + self.focusPanel(panelId) + self.scheduleFocusReconcile() + } + + if !selectionConverged || !anchorReady { + self.scheduleBrowserSplitZoomExitFocusReassert( + panelId: panelId, + remainingPasses: remainingPasses - 1 + ) + } + } + } + private func scheduleMovedTerminalRefresh(panelId: UUID) { guard terminalPanel(for: panelId) != nil else { return }