diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index b5f2bdc3..d3f3b222 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -2759,6 +2759,8 @@ final class WindowBrowserPortal: NSObject { let webViewId = ObjectIdentifier(webView) let anchorId = ObjectIdentifier(anchorView) let previousEntry = entriesByWebViewId[webViewId] + let shouldPreserveExternalFullscreenHost = + webView.cmuxIsManagedByExternalFullscreenWindow(relativeTo: window) let containerView = ensureContainerView( for: previousEntry ?? Entry( webView: nil, @@ -2833,7 +2835,16 @@ final class WindowBrowserPortal: NSObject { } #endif - if webView.superview !== containerView { + if shouldPreserveExternalFullscreenHost { +#if DEBUG + dlog( + "browser.portal.reparent.skip web=\(browserPortalDebugToken(webView)) " + + "reason=fullscreenExternalHost super=\(browserPortalDebugToken(webView.superview)) " + + "container=\(browserPortalDebugToken(containerView)) " + + "state=\(String(describing: webView.fullscreenState))" + ) +#endif + } else if webView.superview !== containerView { #if DEBUG dlog( "browser.portal.reparent web=\(browserPortalDebugToken(webView)) " + @@ -3097,10 +3108,22 @@ final class WindowBrowserPortal: NSObject { hostView.addSubview(containerView, positioned: .above, relativeTo: nil) refreshReasons.append("syncAttachContainer") } + let shouldPreserveExternalFullscreenHost = + webView.cmuxIsManagedByExternalFullscreenWindow(relativeTo: window) let shouldPreserveExternalHostForHiddenEntry = + !shouldPreserveExternalFullscreenHost && !entry.visibleInUI && webView.superview !== containerView - if shouldPreserveExternalHostForHiddenEntry { + if shouldPreserveExternalFullscreenHost { +#if DEBUG + dlog( + "browser.portal.reparent.skip web=\(browserPortalDebugToken(webView)) " + + "reason=fullscreenExternalHost super=\(browserPortalDebugToken(webView.superview)) " + + "container=\(browserPortalDebugToken(containerView)) " + + "state=\(String(describing: webView.fullscreenState))" + ) +#endif + } else if shouldPreserveExternalHostForHiddenEntry { #if DEBUG dlog( "browser.portal.reparent.skip web=\(browserPortalDebugToken(webView)) " + diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index dfb03aa7..4eb218f3 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -2192,6 +2192,7 @@ final class BrowserPanel: Panel, ObservableObject { } } } + @Published private(set) var isElementFullscreenActive: Bool = false private var searchNeedleCancellable: AnyCancellable? let portalAnchorView = BrowserPortalAnchorView(frame: .zero) private struct PortalHostLease { @@ -2478,6 +2479,7 @@ final class BrowserPanel: Panel, ObservableObject { // Enable developer extras (DevTools) configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") + configuration.preferences.isElementFullscreenEnabled = true // Enable JavaScript configuration.defaultWebpagePreferences.allowsContentJavaScript = true @@ -3064,6 +3066,27 @@ final class BrowserPanel: Panel, ObservableObject { } webViewObservers.append(progressObserver) + let fullscreenObserver = webView.observe(\.fullscreenState, options: [.initial, .new]) { [weak self] webView, _ in + let isElementFullscreenActive = webView.cmuxIsElementFullscreenActiveOrTransitioning + let fullscreenState = webView.fullscreenState + Task { @MainActor in + guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } + self.isElementFullscreenActive = isElementFullscreenActive + BrowserWindowPortalRegistry.refresh( + webView: webView, + reason: "fullscreenStateChanged" + ) +#if DEBUG + dlog( + "browser.fullscreen.state panel=\(self.id.uuidString.prefix(5)) " + + "web=\(ObjectIdentifier(webView)) state=\(String(describing: fullscreenState)) " + + "active=\(isElementFullscreenActive ? 1 : 0)" + ) +#endif + } + } + webViewObservers.append(fullscreenObserver) + NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange) .sink { [weak self] notification in guard let self else { return } diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 70694a0e..e8d73a33 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -5774,6 +5774,13 @@ struct WebViewRepresentable: NSViewRepresentable { host.clearLocalInlineCallbacks() } + private static func shouldPreserveExternalFullscreenHost( + for webView: WKWebView, + relativeTo expectedWindow: NSWindow? + ) -> Bool { + webView.cmuxIsManagedByExternalFullscreenWindow(relativeTo: expectedWindow) + } + private static func localInlineTransferRoot(for webView: WKWebView) -> NSView? { var current = webView.superview var last: NSView? @@ -5874,7 +5881,12 @@ struct WebViewRepresentable: NSViewRepresentable { guard let host = nsView as? HostContainerView else { return false } let slotView = host.ensureLocalInlineSlotView() let isAlreadyInLocalHost = host.containsManagedLocalInlineContent(webView) - let didAttachWebViewToLocalHost = !isAlreadyInLocalHost + let shouldPreserveExternalFullscreenHost = Self.shouldPreserveExternalFullscreenHost( + for: webView, + relativeTo: host.window + ) + let didAttachWebViewToLocalHost = + !isAlreadyInLocalHost && !shouldPreserveExternalFullscreenHost let coordinator = context.coordinator coordinator.desiredPortalVisibleInUI = false @@ -5919,6 +5931,16 @@ struct WebViewRepresentable: NSViewRepresentable { return false } +#if DEBUG + if shouldPreserveExternalFullscreenHost { + dlog( + "browser.localHost.reparent.skip web=\(Self.objectID(webView)) " + + "reason=fullscreenExternalHost host=\(Self.objectID(host)) " + + "slot=\(Self.objectID(slotView)) state=\(String(describing: webView.fullscreenState))" + ) + } +#endif + let preferredAttachedWidthState = panel.preferredAttachedDeveloperToolsWidthState() host.setPreferredHostedInspectorWidth( width: preferredAttachedWidthState.width, @@ -5971,7 +5993,7 @@ struct WebViewRepresentable: NSViewRepresentable { host.setHostedInspectorFrontendWebView(webView.cmuxInspectorFrontendWebView()) host.scheduleHostedInspectorDockConfigurationSync(reason: "localInline.update.async") } - } else { + } else if !shouldPreserveExternalFullscreenHost { panel.consumeAttachedDeveloperToolsManualCloseIfNeeded() host.scheduleHostedInspectorDockConfigurationSync(reason: "localInline.update") } @@ -5985,7 +6007,7 @@ struct WebViewRepresentable: NSViewRepresentable { details: Self.attachContext(webView: webView, host: host) ) #endif - return true + return !shouldPreserveExternalFullscreenHost } private func updateUsingWindowPortal(_ nsView: NSView, context: Context, webView: WKWebView) -> Bool { @@ -5993,6 +6015,10 @@ struct WebViewRepresentable: NSViewRepresentable { host.prepareForWindowPortalHosting() host.setLocalInlineSlotHidden(true) host.releaseHostedWebViewConstraints() + let shouldPreserveExternalFullscreenHost = Self.shouldPreserveExternalFullscreenHost( + for: webView, + relativeTo: host.window + ) let coordinator = context.coordinator let paneDropContext = currentPaneDropContext() @@ -6188,7 +6214,7 @@ struct WebViewRepresentable: NSViewRepresentable { details: Self.attachContext(webView: webView, host: host) ) #endif - return portalHostAccepted + return portalHostAccepted && !shouldPreserveExternalFullscreenHost } func updateNSView(_ nsView: NSView, context: Context) { diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift index e913c119..6f5338ca 100644 --- a/Sources/Panels/CmuxWebView.swift +++ b/Sources/Panels/CmuxWebView.swift @@ -4,6 +4,25 @@ import ObjectiveC import UniformTypeIdentifiers import WebKit +extension WKWebView { + var cmuxIsElementFullscreenActiveOrTransitioning: Bool { + switch fullscreenState { + case .notInFullscreen: + return false + case .enteringFullscreen, .inFullscreen, .exitingFullscreen: + return true + @unknown default: + return true + } + } + + func cmuxIsManagedByExternalFullscreenWindow(relativeTo expectedWindow: NSWindow?) -> Bool { + guard cmuxIsElementFullscreenActiveOrTransitioning else { return false } + guard let expectedWindow else { return true } + return window !== expectedWindow + } +} + struct BrowserImageCopyPasteboardPayload { let imageData: Data let mimeType: String?