Fix browser pane portal anchor sizing (#1094)
* Fix browser pane webview sizing * Guard browser portal by pane ownership * Keep browser portal frame during transient zero geometry * Guard portal surfaces against duplicate hosts * Defer terminal surface reflow during tab drags * Fix browser panel pane ID unit tests
This commit is contained in:
parent
a636104fb9
commit
c447bee602
8 changed files with 444 additions and 91 deletions
|
|
@ -1350,6 +1350,8 @@ final class WindowBrowserSlotView: NSView {
|
|||
private let paneDropTargetView = BrowserPaneDropTargetView(frame: .zero)
|
||||
private let dropZoneOverlayView = BrowserDropZoneOverlayView(frame: .zero)
|
||||
private var searchOverlayHostingView: NSHostingView<BrowserSearchOverlay>?
|
||||
private weak var hostedWebView: WKWebView?
|
||||
private var hostedWebViewConstraints: [NSLayoutConstraint] = []
|
||||
private var forwardedDropZone: DropZone?
|
||||
private var portalDragDropZone: DropZone?
|
||||
private var displayedDropZone: DropZone?
|
||||
|
|
@ -1460,6 +1462,34 @@ final class WindowBrowserSlotView: NSView {
|
|||
searchOverlayHostingView = overlay
|
||||
}
|
||||
|
||||
func pinHostedWebView(_ webView: WKWebView) {
|
||||
guard webView.superview === self else { return }
|
||||
|
||||
let needsNewConstraints =
|
||||
hostedWebView !== webView ||
|
||||
hostedWebViewConstraints.isEmpty ||
|
||||
webView.translatesAutoresizingMaskIntoConstraints
|
||||
guard needsNewConstraints else {
|
||||
needsLayout = true
|
||||
layoutSubtreeIfNeeded()
|
||||
return
|
||||
}
|
||||
|
||||
NSLayoutConstraint.deactivate(hostedWebViewConstraints)
|
||||
hostedWebView = webView
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
webView.autoresizingMask = []
|
||||
hostedWebViewConstraints = [
|
||||
webView.topAnchor.constraint(equalTo: topAnchor),
|
||||
webView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
webView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
webView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
]
|
||||
NSLayoutConstraint.activate(hostedWebViewConstraints)
|
||||
needsLayout = true
|
||||
layoutSubtreeIfNeeded()
|
||||
}
|
||||
|
||||
func effectivePaneTopChromeHeight() -> CGFloat {
|
||||
paneTopChromeHeight
|
||||
}
|
||||
|
|
@ -2241,11 +2271,11 @@ final class WindowBrowserPortal: NSObject {
|
|||
} else {
|
||||
containerView.addSubview(webView, positioned: .above, relativeTo: nil)
|
||||
}
|
||||
webView.translatesAutoresizingMaskIntoConstraints = true
|
||||
webView.autoresizingMask = [.width, .height]
|
||||
webView.frame = containerView.bounds
|
||||
containerView.pinHostedWebView(webView)
|
||||
webView.needsLayout = true
|
||||
webView.layoutSubtreeIfNeeded()
|
||||
} else {
|
||||
containerView.pinHostedWebView(webView)
|
||||
}
|
||||
|
||||
if containerView.superview !== hostView {
|
||||
|
|
@ -2496,10 +2526,10 @@ final class WindowBrowserPortal: NSObject {
|
|||
} else {
|
||||
containerView.addSubview(webView, positioned: .above, relativeTo: nil)
|
||||
}
|
||||
webView.translatesAutoresizingMaskIntoConstraints = true
|
||||
webView.autoresizingMask = [.width, .height]
|
||||
webView.frame = containerView.bounds
|
||||
containerView.pinHostedWebView(webView)
|
||||
refreshReasons.append("syncAttachWebView")
|
||||
} else {
|
||||
containerView.pinHostedWebView(webView)
|
||||
}
|
||||
|
||||
_ = synchronizeHostFrameToReference()
|
||||
|
|
@ -2626,12 +2656,23 @@ final class WindowBrowserPortal: NSObject {
|
|||
}
|
||||
#endif
|
||||
if shouldPreserveVisibleOnTransientGeometry {
|
||||
let hasExistingVisibleFrame =
|
||||
oldFrame.width > 1 &&
|
||||
oldFrame.height > 1 &&
|
||||
containerView.bounds.width > 1 &&
|
||||
containerView.bounds.height > 1
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.portal.hidden.deferKeep web=\(browserPortalDebugToken(webView)) " +
|
||||
"reason=\(transientRecoveryReason ?? "unknown") frame=\(browserPortalDebugFrame(containerView.frame))"
|
||||
"reason=\(transientRecoveryReason ?? "unknown") frame=\(browserPortalDebugFrame(containerView.frame)) " +
|
||||
"keepFrame=\(hasExistingVisibleFrame ? 1 : 0)"
|
||||
)
|
||||
#endif
|
||||
if hasExistingVisibleFrame {
|
||||
containerView.setDropZoneOverlay(zone: nil)
|
||||
containerView.setPaneDropContext(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !Self.rectApproximatelyEqual(oldFrame, targetFrame) {
|
||||
CATransaction.begin()
|
||||
|
|
|
|||
|
|
@ -2052,8 +2052,14 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
case closing
|
||||
case closed
|
||||
}
|
||||
private struct PortalHostLease {
|
||||
let hostId: ObjectIdentifier
|
||||
let inWindow: Bool
|
||||
let area: CGFloat
|
||||
}
|
||||
private var portalLifecycleState: PortalLifecycleState = .live
|
||||
private var portalLifecycleGeneration: UInt64 = 1
|
||||
private var activePortalHostLease: PortalHostLease?
|
||||
@Published var searchState: SearchState? = nil {
|
||||
didSet {
|
||||
if let searchState {
|
||||
|
|
@ -2144,6 +2150,90 @@ final class TerminalSurface: Identifiable, ObservableObject {
|
|||
return true
|
||||
}
|
||||
|
||||
private static let portalHostAreaThreshold: CGFloat = 4
|
||||
private static let portalHostReplacementAreaGainRatio: CGFloat = 1.2
|
||||
|
||||
private static func portalHostArea(for bounds: CGRect) -> CGFloat {
|
||||
max(0, bounds.width) * max(0, bounds.height)
|
||||
}
|
||||
|
||||
private static func portalHostIsUsable(_ lease: PortalHostLease) -> Bool {
|
||||
lease.inWindow && lease.area > portalHostAreaThreshold
|
||||
}
|
||||
|
||||
func claimPortalHost(
|
||||
hostId: ObjectIdentifier,
|
||||
inWindow: Bool,
|
||||
bounds: CGRect,
|
||||
reason: String
|
||||
) -> Bool {
|
||||
let next = PortalHostLease(
|
||||
hostId: hostId,
|
||||
inWindow: inWindow,
|
||||
area: Self.portalHostArea(for: bounds)
|
||||
)
|
||||
|
||||
if let current = activePortalHostLease {
|
||||
if current.hostId == hostId {
|
||||
activePortalHostLease = next
|
||||
return true
|
||||
}
|
||||
|
||||
let currentUsable = Self.portalHostIsUsable(current)
|
||||
let nextUsable = Self.portalHostIsUsable(next)
|
||||
let shouldReplace =
|
||||
!currentUsable ||
|
||||
(nextUsable && next.area > (current.area * Self.portalHostReplacementAreaGainRatio))
|
||||
|
||||
if shouldReplace {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"terminal.portal.host.claim surface=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) host=\(hostId) inWin=\(inWindow ? 1 : 0) " +
|
||||
"size=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " +
|
||||
"replacingHost=\(current.hostId) replacingInWin=\(current.inWindow ? 1 : 0) " +
|
||||
"replacingArea=\(String(format: "%.1f", current.area))"
|
||||
)
|
||||
#endif
|
||||
activePortalHostLease = next
|
||||
return true
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"terminal.portal.host.skip surface=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) host=\(hostId) inWin=\(inWindow ? 1 : 0) " +
|
||||
"size=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) " +
|
||||
"ownerHost=\(current.hostId) ownerInWin=\(current.inWindow ? 1 : 0) " +
|
||||
"ownerArea=\(String(format: "%.1f", current.area))"
|
||||
)
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
activePortalHostLease = next
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"terminal.portal.host.claim surface=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) host=\(hostId) inWin=\(inWindow ? 1 : 0) " +
|
||||
"size=\(String(format: "%.1fx%.1f", bounds.width, bounds.height)) replacingHost=nil"
|
||||
)
|
||||
#endif
|
||||
return true
|
||||
}
|
||||
|
||||
func releasePortalHostIfOwned(hostId: ObjectIdentifier, reason: String) {
|
||||
guard let current = activePortalHostLease, current.hostId == hostId else { return }
|
||||
activePortalHostLease = nil
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"terminal.portal.host.release surface=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) host=\(hostId) inWin=\(current.inWindow ? 1 : 0) " +
|
||||
"area=\(String(format: "%.1f", current.area))"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
func beginPortalCloseLifecycle(reason: String) {
|
||||
guard portalLifecycleState != .closed else { return }
|
||||
guard portalLifecycleState != .closing else { return }
|
||||
|
|
@ -2902,6 +2992,8 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
.fileURL,
|
||||
.URL
|
||||
]
|
||||
private static let tabTransferPasteboardType = NSPasteboard.PasteboardType("com.splittabbar.tabtransfer")
|
||||
private static let sidebarTabReorderPasteboardType = NSPasteboard.PasteboardType("com.cmux.sidebar-tab-reorder")
|
||||
private static let shellEscapeCharacters = "\\ ()[]{}<>\"'`!#$&;|*?\t"
|
||||
|
||||
fileprivate static func focusLog(_ message: String) {
|
||||
|
|
@ -3246,6 +3338,11 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
return currentBounds
|
||||
}
|
||||
|
||||
private static func hasActiveTabDragPasteboard() -> Bool {
|
||||
let types = NSPasteboard(name: .drag).types ?? []
|
||||
return types.contains(tabTransferPasteboardType) || types.contains(sidebarTabReorderPasteboardType)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func updateSurfaceSize(size: CGSize? = nil) -> Bool {
|
||||
guard let terminalSurface = terminalSurface else { return false }
|
||||
|
|
@ -3265,6 +3362,20 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
return false
|
||||
}
|
||||
pendingSurfaceSize = size
|
||||
guard !Self.hasActiveTabDragPasteboard() else {
|
||||
#if DEBUG
|
||||
let signature = "tabDrag-\(Int(size.width.rounded()))x\(Int(size.height.rounded()))"
|
||||
if lastSizeSkipSignature != signature {
|
||||
dlog(
|
||||
"surface.size.defer surface=\(terminalSurface.id.uuidString.prefix(5)) reason=tabDrag " +
|
||||
"size=\(String(format: "%.1fx%.1f", size.width, size.height)) " +
|
||||
"inWindow=\(window != nil ? 1 : 0)"
|
||||
)
|
||||
lastSizeSkipSignature = signature
|
||||
}
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
guard let window else {
|
||||
#if DEBUG
|
||||
let signature = "noWindow-\(Int(size.width))x\(Int(size.height))"
|
||||
|
|
@ -5074,6 +5185,13 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
)
|
||||
}
|
||||
|
||||
func releaseOwnedPortalHost(hostId: ObjectIdentifier, reason: String) {
|
||||
surfaceView.terminalSurface?.releasePortalHostIfOwned(
|
||||
hostId: hostId,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
|
||||
init(surfaceView: GhosttyNSView) {
|
||||
self.surfaceView = surfaceView
|
||||
backgroundView = NSView(frame: .zero)
|
||||
|
|
@ -6938,18 +7056,30 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
}
|
||||
#endif
|
||||
|
||||
let hostContainer = nsView as? HostContainerView
|
||||
let hostOwnsPortalNow = hostContainer.map { host in
|
||||
terminalSurface.claimPortalHost(
|
||||
hostId: ObjectIdentifier(host),
|
||||
inWindow: host.window != nil,
|
||||
bounds: host.bounds,
|
||||
reason: "update"
|
||||
)
|
||||
} ?? true
|
||||
|
||||
// Keep the surface lifecycle and handlers updated even if we defer re-parenting.
|
||||
hostedView.attachSurface(terminalSurface)
|
||||
hostedView.setInactiveOverlay(
|
||||
color: inactiveOverlayColor,
|
||||
opacity: CGFloat(inactiveOverlayOpacity),
|
||||
visible: showsInactiveOverlay
|
||||
)
|
||||
hostedView.setNotificationRing(visible: showsUnreadNotificationRing)
|
||||
hostedView.setSearchOverlay(searchState: searchState)
|
||||
hostedView.setKeyboardCopyModeIndicator(visible: terminalSurface.keyboardCopyModeActive)
|
||||
hostedView.setFocusHandler { onFocus?(terminalSurface.id) }
|
||||
hostedView.setTriggerFlashHandler(onTriggerFlash)
|
||||
if hostOwnsPortalNow {
|
||||
hostedView.setInactiveOverlay(
|
||||
color: inactiveOverlayColor,
|
||||
opacity: CGFloat(inactiveOverlayOpacity),
|
||||
visible: showsInactiveOverlay
|
||||
)
|
||||
hostedView.setNotificationRing(visible: showsUnreadNotificationRing)
|
||||
hostedView.setSearchOverlay(searchState: searchState)
|
||||
hostedView.setKeyboardCopyModeIndicator(visible: terminalSurface.keyboardCopyModeActive)
|
||||
}
|
||||
let portalExpectedSurfaceId = terminalSurface.id
|
||||
let portalExpectedGeneration = terminalSurface.portalBindingGeneration()
|
||||
let forwardedDropZone = isVisibleInUI ? paneDropZone : nil
|
||||
|
|
@ -6972,16 +7102,23 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
)
|
||||
}
|
||||
#endif
|
||||
hostedView.setDropZoneOverlay(zone: forwardedDropZone)
|
||||
if hostOwnsPortalNow {
|
||||
hostedView.setDropZoneOverlay(zone: forwardedDropZone)
|
||||
}
|
||||
|
||||
coordinator.attachGeneration += 1
|
||||
let generation = coordinator.attachGeneration
|
||||
|
||||
let hostContainer = nsView as? HostContainerView
|
||||
if let host = hostContainer {
|
||||
host.onDidMoveToWindow = { [weak host, weak hostedView, weak coordinator] in
|
||||
guard let host, let hostedView, let coordinator else { return }
|
||||
guard coordinator.attachGeneration == generation else { return }
|
||||
guard terminalSurface.claimPortalHost(
|
||||
hostId: ObjectIdentifier(host),
|
||||
inWindow: host.window != nil,
|
||||
bounds: host.bounds,
|
||||
reason: "didMoveToWindow"
|
||||
) else { return }
|
||||
guard host.window != nil else { return }
|
||||
TerminalWindowPortalRegistry.bind(
|
||||
hostedView: hostedView,
|
||||
|
|
@ -7000,9 +7137,16 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
host.onGeometryChanged = { [weak host, weak hostedView, weak coordinator] in
|
||||
guard let host, let hostedView, let coordinator else { return }
|
||||
guard coordinator.attachGeneration == generation else { return }
|
||||
guard coordinator.lastBoundHostId == ObjectIdentifier(host) else { return }
|
||||
guard terminalSurface.claimPortalHost(
|
||||
hostId: ObjectIdentifier(host),
|
||||
inWindow: host.window != nil,
|
||||
bounds: host.bounds,
|
||||
reason: "geometryChanged"
|
||||
) else { return }
|
||||
let hostId = ObjectIdentifier(host)
|
||||
if host.window != nil,
|
||||
!TerminalWindowPortalRegistry.isHostedView(hostedView, boundTo: host) {
|
||||
(coordinator.lastBoundHostId != hostId ||
|
||||
!TerminalWindowPortalRegistry.isHostedView(hostedView, boundTo: host)) {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"ws.hostState.rebindOnGeometry surface=\(terminalSurface.id.uuidString.prefix(5)) " +
|
||||
|
|
@ -7018,7 +7162,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
expectedSurfaceId: portalExpectedSurfaceId,
|
||||
expectedGeneration: portalExpectedGeneration
|
||||
)
|
||||
coordinator.lastBoundHostId = ObjectIdentifier(host)
|
||||
coordinator.lastBoundHostId = hostId
|
||||
hostedView.setVisibleInUI(coordinator.desiredIsVisibleInUI)
|
||||
hostedView.setActive(coordinator.desiredIsActive)
|
||||
hostedView.setNotificationRing(visible: coordinator.desiredShowsUnreadNotificationRing)
|
||||
|
|
@ -7027,7 +7171,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
coordinator.lastSynchronizedHostGeometryRevision = host.geometryRevision
|
||||
}
|
||||
|
||||
if host.window != nil {
|
||||
if host.window != nil, hostOwnsPortalNow {
|
||||
let hostId = ObjectIdentifier(host)
|
||||
let geometryRevision = host.geometryRevision
|
||||
let portalEntryMissing = !TerminalWindowPortalRegistry.isHostedView(hostedView, boundTo: host)
|
||||
|
|
@ -7062,7 +7206,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
TerminalWindowPortalRegistry.synchronizeForAnchor(host)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
|
||||
}
|
||||
} else {
|
||||
} else if hostOwnsPortalNow {
|
||||
// Bind is deferred until host moves into a window. Update the
|
||||
// existing portal entry's visibleInUI now so that any portal sync
|
||||
// that runs before the deferred bind completes won't hide the view.
|
||||
|
|
@ -7087,7 +7231,7 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
let isBoundToCurrentHost = hostContainer.map { host in
|
||||
TerminalWindowPortalRegistry.isHostedView(hostedView, boundTo: host)
|
||||
} ?? true
|
||||
let shouldApplyImmediateHostedState = Self.shouldApplyImmediateHostedStateUpdate(
|
||||
let shouldApplyImmediateHostedState = hostOwnsPortalNow && Self.shouldApplyImmediateHostedStateUpdate(
|
||||
hostedViewHasSuperview: hostedView.superview != nil,
|
||||
isBoundToCurrentHost: isBoundToCurrentHost
|
||||
)
|
||||
|
|
@ -7102,7 +7246,8 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
if desiredStateChanged {
|
||||
dlog(
|
||||
"ws.hostState.deferApply surface=\(terminalSurface.id.uuidString.prefix(5)) " +
|
||||
"reason=staleHostBinding hostWindow=\(hostWindowAttached ? 1 : 0) " +
|
||||
"reason=\(hostOwnsPortalNow ? "staleHostBinding" : "hostOwnershipRejected") " +
|
||||
"hostWindow=\(hostWindowAttached ? 1 : 0) " +
|
||||
"boundToCurrent=\(isBoundToCurrentHost ? 1 : 0) hostedSuperview=\(hostedView.superview != nil ? 1 : 0) " +
|
||||
"visible=\(isVisibleInUI ? 1 : 0) active=\(isActive ? 1 : 0)"
|
||||
)
|
||||
|
|
@ -7140,6 +7285,10 @@ struct GhosttyTerminalView: NSViewRepresentable {
|
|||
if let host = nsView as? HostContainerView {
|
||||
host.onDidMoveToWindow = nil
|
||||
host.onGeometryChanged = nil
|
||||
hostedView?.releaseOwnedPortalHost(
|
||||
hostId: ObjectIdentifier(host),
|
||||
reason: "dismantle"
|
||||
)
|
||||
}
|
||||
|
||||
// SwiftUI can transiently dismantle/rebuild NSViewRepresentable instances during split
|
||||
|
|
|
|||
|
|
@ -1714,6 +1714,13 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
}
|
||||
private var searchNeedleCancellable: AnyCancellable?
|
||||
let portalAnchorView = BrowserPortalAnchorView(frame: .zero)
|
||||
private struct PortalHostLease {
|
||||
let hostId: ObjectIdentifier
|
||||
let paneId: UUID
|
||||
let inWindow: Bool
|
||||
let area: CGFloat
|
||||
}
|
||||
private var activePortalHostLease: PortalHostLease?
|
||||
private var webViewCancellables = Set<AnyCancellable>()
|
||||
private var navigationDelegate: BrowserNavigationDelegate?
|
||||
private var uiDelegate: BrowserUIDelegate?
|
||||
|
|
@ -1755,6 +1762,94 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
return String(localized: "browser.newTab", defaultValue: "New tab")
|
||||
}
|
||||
|
||||
private static let portalHostAreaThreshold: CGFloat = 4
|
||||
private static let portalHostReplacementAreaGainRatio: CGFloat = 1.2
|
||||
|
||||
private static func portalHostArea(for bounds: CGRect) -> CGFloat {
|
||||
max(0, bounds.width) * max(0, bounds.height)
|
||||
}
|
||||
|
||||
private static func portalHostIsUsable(_ lease: PortalHostLease) -> Bool {
|
||||
lease.inWindow && lease.area > portalHostAreaThreshold
|
||||
}
|
||||
|
||||
func claimPortalHost(
|
||||
hostId: ObjectIdentifier,
|
||||
paneId: PaneID,
|
||||
inWindow: Bool,
|
||||
bounds: CGRect,
|
||||
reason: String
|
||||
) -> Bool {
|
||||
let next = PortalHostLease(
|
||||
hostId: hostId,
|
||||
paneId: paneId.id,
|
||||
inWindow: inWindow,
|
||||
area: Self.portalHostArea(for: bounds)
|
||||
)
|
||||
|
||||
if let current = activePortalHostLease {
|
||||
if current.hostId == hostId {
|
||||
activePortalHostLease = next
|
||||
return true
|
||||
}
|
||||
|
||||
let currentUsable = Self.portalHostIsUsable(current)
|
||||
let nextUsable = Self.portalHostIsUsable(next)
|
||||
let shouldReplace =
|
||||
current.paneId != paneId.id ||
|
||||
!currentUsable ||
|
||||
(nextUsable && next.area > (current.area * Self.portalHostReplacementAreaGainRatio))
|
||||
|
||||
if shouldReplace {
|
||||
#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))"
|
||||
)
|
||||
#endif
|
||||
activePortalHostLease = next
|
||||
return true
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.portal.host.skip 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)) " +
|
||||
"ownerHost=\(current.hostId) ownerPane=\(current.paneId.uuidString.prefix(5)) " +
|
||||
"ownerInWin=\(current.inWindow ? 1 : 0) ownerArea=\(String(format: "%.1f", current.area))"
|
||||
)
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
activePortalHostLease = next
|
||||
#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=nil"
|
||||
)
|
||||
#endif
|
||||
return true
|
||||
}
|
||||
|
||||
func releasePortalHostIfOwned(hostId: ObjectIdentifier, reason: String) {
|
||||
guard let current = activePortalHostLease, current.hostId == hostId else { return }
|
||||
activePortalHostLease = nil
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.portal.host.release panel=\(id.uuidString.prefix(5)) " +
|
||||
"reason=\(reason) host=\(hostId) pane=\(current.paneId.uuidString.prefix(5)) " +
|
||||
"inWin=\(current.inWindow ? 1 : 0) area=\(String(format: "%.1f", current.area))"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
var displayIcon: String? {
|
||||
"globe"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ func resolvedBrowserOmnibarPillBackgroundColor(
|
|||
/// View for rendering a browser panel with address bar
|
||||
struct BrowserPanelView: View {
|
||||
@ObservedObject var panel: BrowserPanel
|
||||
let paneId: PaneID
|
||||
let isFocused: Bool
|
||||
let isVisibleInUI: Bool
|
||||
let portalPriority: Int
|
||||
|
|
@ -312,13 +313,23 @@ struct BrowserPanelView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var isCurrentPaneOwner: Bool {
|
||||
guard let workspace = AppDelegate.shared?.tabManager?.tabs.first(where: { $0.id == panel.workspaceId }),
|
||||
let currentPaneId = workspace.paneId(forPanelId: panel.id) else {
|
||||
return false
|
||||
}
|
||||
return currentPaneId.id == paneId.id
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Layering contract: browser Cmd+F UI is mounted in the portal-hosted AppKit
|
||||
// container. Rendering it here can hide it behind the portal-hosted WKWebView.
|
||||
VStack(spacing: 0) {
|
||||
addressBar
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
webView
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.overlay {
|
||||
// Keep Cmd+F usable when the browser is still in the empty new-tab
|
||||
// state (no WKWebView mounted yet). WebView-backed cases are hosted
|
||||
|
|
@ -795,7 +806,8 @@ struct BrowserPanelView: View {
|
|||
if panel.shouldRenderWebView {
|
||||
WebViewRepresentable(
|
||||
panel: panel,
|
||||
shouldAttachWebView: isVisibleInUI,
|
||||
paneId: paneId,
|
||||
shouldAttachWebView: isVisibleInUI && isCurrentPaneOwner,
|
||||
shouldFocusWebView: isFocused && !addressBarFocused,
|
||||
isPanelFocused: isFocused,
|
||||
portalZPriority: portalPriority,
|
||||
|
|
@ -813,8 +825,9 @@ struct BrowserPanelView: View {
|
|||
)
|
||||
.accessibilityIdentifier("BrowserWebViewSurface")
|
||||
// Keep the host stable for normal pane churn, but force a remount when
|
||||
// BrowserPanel replaces its underlying WKWebView after process termination.
|
||||
.id(panel.webViewInstanceID)
|
||||
// BrowserPanel replaces its underlying WKWebView after process termination
|
||||
// or when the browser moves to a different Bonsplit pane host.
|
||||
.id("\(panel.webViewInstanceID.uuidString)-\(paneId.id.uuidString)")
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityIdentifier(browserContentAccessibilityIdentifier)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
|
|
@ -839,6 +852,8 @@ struct BrowserPanelView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.layoutPriority(1)
|
||||
.zIndex(0)
|
||||
}
|
||||
|
||||
|
|
@ -3502,6 +3517,7 @@ private struct OmnibarSuggestionsView: View {
|
|||
/// NSViewRepresentable wrapper for WKWebView
|
||||
struct WebViewRepresentable: NSViewRepresentable {
|
||||
let panel: BrowserPanel
|
||||
let paneId: PaneID
|
||||
let shouldAttachWebView: Bool
|
||||
let shouldFocusWebView: Bool
|
||||
let isPanelFocused: Bool
|
||||
|
|
@ -4271,35 +4287,83 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
guard host.window != nil else { return }
|
||||
if anchorView.superview !== host {
|
||||
anchorView.removeFromSuperview()
|
||||
anchorView.frame = host.bounds
|
||||
anchorView.translatesAutoresizingMaskIntoConstraints = true
|
||||
anchorView.autoresizingMask = [.width, .height]
|
||||
anchorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
host.addSubview(anchorView)
|
||||
} else if anchorView.frame != host.bounds {
|
||||
anchorView.frame = host.bounds
|
||||
NSLayoutConstraint.activate([
|
||||
anchorView.topAnchor.constraint(equalTo: host.topAnchor),
|
||||
anchorView.bottomAnchor.constraint(equalTo: host.bottomAnchor),
|
||||
anchorView.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
||||
anchorView.trailingAnchor.constraint(equalTo: host.trailingAnchor),
|
||||
])
|
||||
} else if anchorView.translatesAutoresizingMaskIntoConstraints {
|
||||
anchorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
anchorView.topAnchor.constraint(equalTo: host.topAnchor),
|
||||
anchorView.bottomAnchor.constraint(equalTo: host.bottomAnchor),
|
||||
anchorView.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
||||
anchorView.trailingAnchor.constraint(equalTo: host.trailingAnchor),
|
||||
])
|
||||
}
|
||||
host.layoutSubtreeIfNeeded()
|
||||
}
|
||||
|
||||
private func updateUsingWindowPortal(_ nsView: NSView, context: Context, webView: WKWebView) {
|
||||
guard let host = nsView as? HostContainerView else { return }
|
||||
private func updateUsingWindowPortal(_ nsView: NSView, context: Context, webView: WKWebView) -> Bool {
|
||||
guard let host = nsView as? HostContainerView else { return false }
|
||||
|
||||
let coordinator = context.coordinator
|
||||
let paneDropContext = currentPaneDropContext()
|
||||
let isCurrentPaneOwner = paneDropContext?.paneId.id == paneId.id
|
||||
let hostId = ObjectIdentifier(host)
|
||||
let previousVisible = coordinator.desiredPortalVisibleInUI
|
||||
let previousZPriority = coordinator.desiredPortalZPriority
|
||||
coordinator.desiredPortalVisibleInUI = shouldAttachWebView
|
||||
coordinator.desiredPortalVisibleInUI = shouldAttachWebView && isCurrentPaneOwner
|
||||
coordinator.desiredPortalZPriority = portalZPriority
|
||||
coordinator.attachGeneration += 1
|
||||
let generation = coordinator.attachGeneration
|
||||
let paneDropContext = shouldAttachWebView ? currentPaneDropContext() : nil
|
||||
let activeSearchOverlay = shouldAttachWebView ? searchOverlay : nil
|
||||
let activePaneDropContext = coordinator.desiredPortalVisibleInUI ? paneDropContext : nil
|
||||
let activeSearchOverlay = coordinator.desiredPortalVisibleInUI ? searchOverlay : nil
|
||||
let portalAnchorView = panel.portalAnchorView
|
||||
if host.window != nil {
|
||||
if !shouldAttachWebView || !isCurrentPaneOwner {
|
||||
panel.releasePortalHostIfOwned(
|
||||
hostId: hostId,
|
||||
reason: !isCurrentPaneOwner ? "lostPaneOwnership" : "hidden"
|
||||
)
|
||||
}
|
||||
let portalHostAccepted =
|
||||
shouldAttachWebView &&
|
||||
isCurrentPaneOwner &&
|
||||
panel.claimPortalHost(
|
||||
hostId: hostId,
|
||||
paneId: paneId,
|
||||
inWindow: host.window != nil,
|
||||
bounds: host.bounds,
|
||||
reason: "update"
|
||||
)
|
||||
#if DEBUG
|
||||
if !isCurrentPaneOwner && (shouldAttachWebView || host.window != nil) {
|
||||
dlog(
|
||||
"browser.portal.owner.skip panel=\(panel.id.uuidString.prefix(5)) " +
|
||||
"viewPane=\(paneId.id.uuidString.prefix(5)) " +
|
||||
"currentPane=\(paneDropContext?.paneId.id.uuidString.prefix(5) ?? "nil") " +
|
||||
"host=\(Self.objectID(host)) hostInWin=\(host.window != nil ? 1 : 0)"
|
||||
)
|
||||
}
|
||||
#endif
|
||||
if host.window != nil, portalHostAccepted {
|
||||
Self.installPortalAnchorView(portalAnchorView, in: host)
|
||||
}
|
||||
|
||||
host.onDidMoveToWindow = { [weak host, weak webView, weak coordinator, weak portalAnchorView] in
|
||||
guard let host, let webView, let coordinator, let portalAnchorView else { return }
|
||||
host.onDidMoveToWindow = { [weak host, weak webView, weak coordinator, weak portalAnchorView, weak browserPanel = panel] in
|
||||
guard let host, let webView, let coordinator, let portalAnchorView, let browserPanel else { return }
|
||||
guard coordinator.attachGeneration == generation else { return }
|
||||
guard currentPaneDropContext()?.paneId.id == paneId.id else { return }
|
||||
guard browserPanel.claimPortalHost(
|
||||
hostId: ObjectIdentifier(host),
|
||||
paneId: paneId,
|
||||
inWindow: host.window != nil,
|
||||
bounds: host.bounds,
|
||||
reason: "didMoveToWindow"
|
||||
) else { return }
|
||||
guard host.window != nil else { return }
|
||||
Self.installPortalAnchorView(portalAnchorView, in: host)
|
||||
BrowserWindowPortalRegistry.bind(
|
||||
|
|
@ -4312,18 +4376,26 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
for: webView,
|
||||
height: coordinator.desiredPortalVisibleInUI ? paneTopChromeHeight : 0
|
||||
)
|
||||
BrowserWindowPortalRegistry.updatePaneDropContext(for: webView, context: paneDropContext)
|
||||
BrowserWindowPortalRegistry.updatePaneDropContext(for: webView, context: activePaneDropContext)
|
||||
BrowserWindowPortalRegistry.updateSearchOverlay(for: webView, configuration: activeSearchOverlay)
|
||||
coordinator.lastPortalHostId = ObjectIdentifier(host)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = host.geometryRevision
|
||||
}
|
||||
host.onGeometryChanged = { [weak host, weak webView, weak coordinator, weak portalAnchorView] in
|
||||
guard let host, let webView, let coordinator, let portalAnchorView else { return }
|
||||
host.onGeometryChanged = { [weak host, weak webView, weak coordinator, weak portalAnchorView, weak browserPanel = panel] in
|
||||
guard let host, let webView, let coordinator, let portalAnchorView, let browserPanel else { return }
|
||||
guard coordinator.attachGeneration == generation else { return }
|
||||
guard coordinator.lastPortalHostId == ObjectIdentifier(host) else { return }
|
||||
guard currentPaneDropContext()?.paneId.id == paneId.id else { return }
|
||||
guard browserPanel.claimPortalHost(
|
||||
hostId: ObjectIdentifier(host),
|
||||
paneId: paneId,
|
||||
inWindow: host.window != nil,
|
||||
bounds: host.bounds,
|
||||
reason: "geometryChanged"
|
||||
) else { return }
|
||||
guard host.window != nil else { return }
|
||||
let hostId = ObjectIdentifier(host)
|
||||
Self.installPortalAnchorView(portalAnchorView, in: host)
|
||||
if host.window != nil,
|
||||
if coordinator.lastPortalHostId != hostId ||
|
||||
!BrowserWindowPortalRegistry.isWebView(webView, boundTo: portalAnchorView) {
|
||||
BrowserWindowPortalRegistry.bind(
|
||||
webView: webView,
|
||||
|
|
@ -4335,9 +4407,9 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
for: webView,
|
||||
height: coordinator.desiredPortalVisibleInUI ? paneTopChromeHeight : 0
|
||||
)
|
||||
BrowserWindowPortalRegistry.updatePaneDropContext(for: webView, context: paneDropContext)
|
||||
BrowserWindowPortalRegistry.updatePaneDropContext(for: webView, context: activePaneDropContext)
|
||||
BrowserWindowPortalRegistry.updateSearchOverlay(for: webView, configuration: activeSearchOverlay)
|
||||
coordinator.lastPortalHostId = ObjectIdentifier(host)
|
||||
coordinator.lastPortalHostId = hostId
|
||||
}
|
||||
BrowserWindowPortalRegistry.synchronizeForAnchor(portalAnchorView)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = host.geometryRevision
|
||||
|
|
@ -4349,8 +4421,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
panel.syncDeveloperToolsPreferenceFromInspector()
|
||||
}
|
||||
|
||||
if host.window != nil {
|
||||
let hostId = ObjectIdentifier(host)
|
||||
if host.window != nil, portalHostAccepted {
|
||||
let geometryRevision = host.geometryRevision
|
||||
let portalEntryMissing = !BrowserWindowPortalRegistry.isWebView(webView, boundTo: portalAnchorView)
|
||||
let shouldBindNow =
|
||||
|
|
@ -4372,7 +4443,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
}
|
||||
BrowserWindowPortalRegistry.updatePaneTopChromeHeight(
|
||||
for: webView,
|
||||
height: shouldAttachWebView ? paneTopChromeHeight : 0
|
||||
height: coordinator.desiredPortalVisibleInUI ? paneTopChromeHeight : 0
|
||||
)
|
||||
BrowserWindowPortalRegistry.updateSearchOverlay(for: webView, configuration: activeSearchOverlay)
|
||||
if !shouldBindNow,
|
||||
|
|
@ -4380,7 +4451,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
BrowserWindowPortalRegistry.synchronizeForAnchor(portalAnchorView)
|
||||
coordinator.lastSynchronizedHostGeometryRevision = geometryRevision
|
||||
}
|
||||
} else {
|
||||
} else if portalHostAccepted {
|
||||
// Bind is deferred until host moves into a window. Keep the current
|
||||
// portal entry's desired state in sync so stale callbacks cannot keep
|
||||
// the previous anchor visible while this host is temporarily off-window.
|
||||
|
|
@ -4391,19 +4462,21 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
)
|
||||
}
|
||||
|
||||
BrowserWindowPortalRegistry.updateDropZoneOverlay(
|
||||
for: webView,
|
||||
zone: shouldAttachWebView ? paneDropZone : nil
|
||||
)
|
||||
BrowserWindowPortalRegistry.updatePaneTopChromeHeight(
|
||||
for: webView,
|
||||
height: shouldAttachWebView ? paneTopChromeHeight : 0
|
||||
)
|
||||
BrowserWindowPortalRegistry.updatePaneDropContext(
|
||||
for: webView,
|
||||
context: paneDropContext
|
||||
)
|
||||
BrowserWindowPortalRegistry.updateSearchOverlay(for: webView, configuration: activeSearchOverlay)
|
||||
if portalHostAccepted {
|
||||
BrowserWindowPortalRegistry.updateDropZoneOverlay(
|
||||
for: webView,
|
||||
zone: coordinator.desiredPortalVisibleInUI ? paneDropZone : nil
|
||||
)
|
||||
BrowserWindowPortalRegistry.updatePaneTopChromeHeight(
|
||||
for: webView,
|
||||
height: coordinator.desiredPortalVisibleInUI ? paneTopChromeHeight : 0
|
||||
)
|
||||
BrowserWindowPortalRegistry.updatePaneDropContext(
|
||||
for: webView,
|
||||
context: activePaneDropContext
|
||||
)
|
||||
BrowserWindowPortalRegistry.updateSearchOverlay(for: webView, configuration: activeSearchOverlay)
|
||||
}
|
||||
|
||||
panel.restoreDeveloperToolsAfterAttachIfNeeded()
|
||||
|
||||
|
|
@ -4416,11 +4489,13 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
details: Self.attachContext(webView: webView, host: host)
|
||||
)
|
||||
#endif
|
||||
return portalHostAccepted
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) {
|
||||
let webView = panel.webView
|
||||
let coordinator = context.coordinator
|
||||
let isCurrentPaneOwner = currentPaneDropContext()?.paneId.id == paneId.id
|
||||
if let previousWebView = coordinator.webView, previousWebView !== webView {
|
||||
BrowserWindowPortalRegistry.detach(webView: previousWebView)
|
||||
coordinator.lastPortalHostId = nil
|
||||
|
|
@ -4428,21 +4503,21 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
}
|
||||
coordinator.panel = panel
|
||||
coordinator.webView = webView
|
||||
|
||||
Self.clearPortalCallbacks(for: nsView)
|
||||
let hostOwnsPortal = updateUsingWindowPortal(nsView, context: context, webView: webView)
|
||||
Self.applyWebViewFirstResponderPolicy(
|
||||
panel: panel,
|
||||
webView: webView,
|
||||
isPanelFocused: isPanelFocused
|
||||
isPanelFocused: isPanelFocused && isCurrentPaneOwner && hostOwnsPortal
|
||||
)
|
||||
|
||||
Self.clearPortalCallbacks(for: nsView)
|
||||
updateUsingWindowPortal(nsView, context: context, webView: webView)
|
||||
|
||||
Self.applyFocus(
|
||||
panel: panel,
|
||||
webView: webView,
|
||||
nsView: nsView,
|
||||
shouldFocusWebView: shouldFocusWebView,
|
||||
isPanelFocused: isPanelFocused
|
||||
shouldFocusWebView: shouldFocusWebView && isCurrentPaneOwner && hostOwnsPortal,
|
||||
isPanelFocused: isPanelFocused && isCurrentPaneOwner && hostOwnsPortal
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -4527,6 +4602,12 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) {
|
||||
coordinator.attachGeneration += 1
|
||||
clearPortalCallbacks(for: nsView)
|
||||
if let panel = coordinator.panel, let host = nsView as? HostContainerView {
|
||||
panel.releasePortalHostIfOwned(
|
||||
hostId: ObjectIdentifier(host),
|
||||
reason: "dismantle"
|
||||
)
|
||||
}
|
||||
|
||||
guard let webView = coordinator.webView else { return }
|
||||
let panel = coordinator.panel
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import SwiftUI
|
||||
import Foundation
|
||||
import Bonsplit
|
||||
|
||||
/// View that renders the appropriate panel view based on panel type
|
||||
struct PanelContentView: View {
|
||||
let panel: any Panel
|
||||
let paneId: PaneID
|
||||
let isFocused: Bool
|
||||
let isSelectedInPane: Bool
|
||||
let isVisibleInUI: Bool
|
||||
|
|
@ -35,6 +37,7 @@ struct PanelContentView: View {
|
|||
if let browserPanel = panel as? BrowserPanel {
|
||||
BrowserPanelView(
|
||||
panel: browserPanel,
|
||||
paneId: paneId,
|
||||
isFocused: isFocused,
|
||||
isVisibleInUI: isVisibleInUI,
|
||||
portalPriority: portalPriority,
|
||||
|
|
|
|||
|
|
@ -2924,7 +2924,6 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
scheduleFocusReconcile()
|
||||
}
|
||||
scheduleTerminalGeometryReconcile()
|
||||
scheduleMovedBrowserRefresh(panelId: detached.panelId)
|
||||
|
||||
#if DEBUG
|
||||
dlog(
|
||||
|
|
@ -3509,25 +3508,6 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
runRefreshPass(0.03)
|
||||
}
|
||||
|
||||
private func scheduleMovedBrowserRefresh(panelId: UUID) {
|
||||
guard browserPanel(for: panelId) != nil else { return }
|
||||
|
||||
let runRefreshPass: (TimeInterval) -> Void = { [weak self] delay in
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
||||
guard let self, let browser = self.browserPanel(for: panelId) else { return }
|
||||
BrowserWindowPortalRegistry.refresh(
|
||||
webView: browser.webView,
|
||||
reason: "workspace.movedBrowserRefresh"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror terminal moved-surface refreshes so round-trip pane drags get
|
||||
// another render pass after bonsplit has settled its reparenting.
|
||||
runRefreshPass(0)
|
||||
runRefreshPass(0.03)
|
||||
}
|
||||
|
||||
private func closeTabs(_ tabIds: [TabID], skipPinned: Bool = true) {
|
||||
for tabId in tabIds {
|
||||
if skipPinned,
|
||||
|
|
@ -4195,7 +4175,6 @@ extension Workspace: BonsplitDelegate {
|
|||
#endif
|
||||
if let movedPanelId = panelIdFromSurfaceId(tab.id) {
|
||||
scheduleMovedTerminalRefresh(panelId: movedPanelId)
|
||||
scheduleMovedBrowserRefresh(panelId: movedPanelId)
|
||||
}
|
||||
#if DEBUG
|
||||
let selectedAfter = controller.selectedTab(inPane: destination)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ struct WorkspaceContentView: View {
|
|||
)
|
||||
PanelContentView(
|
||||
panel: panel,
|
||||
paneId: paneId,
|
||||
isFocused: isFocused,
|
||||
isSelectedInPane: isSelectedInPane,
|
||||
isVisibleInUI: isVisibleInUI,
|
||||
|
|
|
|||
|
|
@ -2513,6 +2513,7 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
|
||||
func testWebViewDismantleKeepsPortalHostedWebViewAttachedWhenDeveloperToolsIntentIsVisible() {
|
||||
let (panel, _) = makePanelWithInspector()
|
||||
let paneId = PaneID(id: UUID())
|
||||
XCTAssertTrue(panel.showDeveloperTools())
|
||||
|
||||
let window = NSWindow(
|
||||
|
|
@ -2534,6 +2535,7 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
|
||||
let representable = WebViewRepresentable(
|
||||
panel: panel,
|
||||
paneId: paneId,
|
||||
shouldAttachWebView: true,
|
||||
shouldFocusWebView: false,
|
||||
isPanelFocused: true,
|
||||
|
|
@ -2552,6 +2554,7 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
|
||||
func testWebViewDismantleKeepsPortalHostedWebViewAttachedWhenDeveloperToolsIntentIsHidden() {
|
||||
let (panel, _) = makePanelWithInspector()
|
||||
let paneId = PaneID(id: UUID())
|
||||
XCTAssertFalse(panel.shouldPreserveWebViewAttachmentDuringTransientHide())
|
||||
|
||||
let window = NSWindow(
|
||||
|
|
@ -2573,6 +2576,7 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
|
||||
let representable = WebViewRepresentable(
|
||||
panel: panel,
|
||||
paneId: paneId,
|
||||
shouldAttachWebView: true,
|
||||
shouldFocusWebView: false,
|
||||
isPanelFocused: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue