diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 1de11528..5c2d7cd8 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -4162,7 +4162,7 @@ private extension BrowserPanel { } } -private extension WKWebView { +extension WKWebView { func cmuxInspectorObject() -> NSObject? { let selector = NSSelectorFromString("_inspector") guard responds(to: selector), @@ -4171,6 +4171,16 @@ private extension WKWebView { } return inspector } + + func cmuxInspectorFrontendWebView() -> WKWebView? { + guard let inspector = cmuxInspectorObject() else { return nil } + let selector = NSSelectorFromString("inspectorWebView") + guard inspector.responds(to: selector), + let inspectorWebView = inspector.perform(selector)?.takeUnretainedValue() as? WKWebView else { + return nil + } + return inspectorWebView + } } private extension NSObject { diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index c0f81e76..f1c4bbae 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -3579,6 +3579,10 @@ struct WebViewRepresentable: NSViewRepresentable { } final class HostContainerView: NSView { + private final class HostedInspectorSideDockContainerView: NSView { + override var isOpaque: Bool { false } + } + var onDidMoveToWindow: (() -> Void)? var onGeometryChanged: (() -> Void)? private(set) var geometryRevision: UInt64 = 0 @@ -3587,6 +3591,9 @@ struct WebViewRepresentable: NSViewRepresentable { private var hostedWebViewConstraints: [NSLayoutConstraint] = [] private weak var localInlineSlotView: WindowBrowserSlotView? private var localInlineSlotConstraints: [NSLayoutConstraint] = [] + private weak var hostedInspectorSideDockContainerView: HostedInspectorSideDockContainerView? + private var hostedInspectorSideDockConstraints: [NSLayoutConstraint] = [] + private weak var hostedInspectorFrontendWebView: WKWebView? private struct HostedInspectorDividerHit { let containerView: NSView let pageView: NSView @@ -3617,7 +3624,7 @@ struct WebViewRepresentable: NSViewRepresentable { var cursor: NSCursor { .resizeLeftRight } } - private static let hostedInspectorDividerHitExpansion: CGFloat = 6 + private static let hostedInspectorDividerHitExpansion: CGFloat = 10 private static let minimumHostedInspectorWidth: CGFloat = 1 private var trackingArea: NSTrackingArea? private var activeDividerCursorKind: DividerCursorKind? @@ -3625,9 +3632,13 @@ struct WebViewRepresentable: NSViewRepresentable { private var preferredHostedInspectorWidth: CGFloat? private var preferredHostedInspectorWidthFraction: CGFloat? var onPreferredHostedInspectorWidthChanged: ((CGFloat, CGFloat?) -> Void)? + private weak var hostedInspectorSideDockPageView: NSView? + private weak var hostedInspectorSideDockInspectorView: NSView? + private var hostedInspectorSideDockDockSide: HostedInspectorDockSide? private var isHostedInspectorDividerDragActive = false private var isApplyingHostedInspectorLayout = false private var hostedInspectorReapplyWorkItem: DispatchWorkItem? + private var hostedInspectorDockConfigurationSyncWorkItem: DispatchWorkItem? private var lastHostedInspectorLayoutBoundsSize: NSSize? #if DEBUG private var lastLoggedHostedInspectorFrames: (page: NSRect, inspector: NSRect)? @@ -3636,6 +3647,7 @@ struct WebViewRepresentable: NSViewRepresentable { deinit { hostedInspectorReapplyWorkItem?.cancel() + hostedInspectorDockConfigurationSyncWorkItem?.cancel() if let trackingArea { removeTrackingArea(trackingArea) } @@ -3665,6 +3677,36 @@ struct WebViewRepresentable: NSViewRepresentable { preferredHostedInspectorWidthFraction = widthFraction } + func containsManagedLocalInlineContent(_ view: NSView) -> Bool { + if let localInlineSlotView, + view === localInlineSlotView || view.isDescendant(of: localInlineSlotView) { + return true + } + if let hostedInspectorSideDockContainerView, + view === hostedInspectorSideDockContainerView || view.isDescendant(of: hostedInspectorSideDockContainerView) { + return true + } + return false + } + + func currentHostedWebViewContainer(preferredSlotView: WindowBrowserSlotView) -> NSView { + if let hostedInspectorSideDockContainerView, + let hostedInspectorSideDockPageView, + hostedWebView?.isDescendant(of: hostedInspectorSideDockContainerView) == true, + hostedInspectorSideDockPageView.isDescendant(of: hostedInspectorSideDockContainerView) { + return hostedInspectorSideDockContainerView + } + return preferredSlotView + } + + func setHostedInspectorFrontendWebView(_ webView: WKWebView?) { + hostedInspectorFrontendWebView = webView + } + + private var hasStoredHostedInspectorWidthPreference: Bool { + preferredHostedInspectorWidth != nil || preferredHostedInspectorWidthFraction != nil + } + #if DEBUG private static func shouldLogPointerEvent(_ event: NSEvent?) -> Bool { switch event?.type { @@ -3812,6 +3854,13 @@ struct WebViewRepresentable: NSViewRepresentable { localInlineSlotView?.onHostedInspectorLayout = nil } + func prepareForWindowPortalHosting() { + hostedInspectorDockConfigurationSyncWorkItem?.cancel() + hostedInspectorDockConfigurationSyncWorkItem = nil + deactivateHostedInspectorSideDockIfNeeded(reparentTo: localInlineSlotView) + hostedInspectorFrontendWebView = nil + } + func releaseHostedWebViewConstraints() { NSLayoutConstraint.deactivate(hostedWebViewConstraints) hostedWebViewConstraints = [] @@ -3819,13 +3868,14 @@ struct WebViewRepresentable: NSViewRepresentable { } func pinHostedWebView(_ webView: WKWebView, in container: NSView) { - guard webView.superview === container else { return } + guard webView.superview === container || webView.isDescendant(of: container) else { return } let hasCompanionWKSubviews = Self.hasWebKitCompanionSubview( in: container, primaryWebView: webView ) let needsPlainWebViewFrameReset = + webView.superview === container && !hasCompanionWKSubviews && Self.frameDiffersFromBounds(webView.frame, bounds: container.bounds) let needsFrameHosting = @@ -3849,7 +3899,7 @@ struct WebViewRepresentable: NSViewRepresentable { // preserve WebKit-managed split frames when docked DevTools siblings exist. webView.translatesAutoresizingMaskIntoConstraints = true webView.autoresizingMask = [.width, .height] - if !hasCompanionWKSubviews { + if webView.superview === container && !hasCompanionWKSubviews { webView.frame = container.bounds } needsLayout = true @@ -3877,12 +3927,159 @@ struct WebViewRepresentable: NSViewRepresentable { return false } + private func ensureHostedInspectorSideDockContainerView() -> HostedInspectorSideDockContainerView { + if let hostedInspectorSideDockContainerView, + hostedInspectorSideDockContainerView.superview === self { + hostedInspectorSideDockContainerView.isHidden = false + return hostedInspectorSideDockContainerView + } + + let containerView = HostedInspectorSideDockContainerView(frame: bounds) + containerView.translatesAutoresizingMaskIntoConstraints = false + addSubview(containerView, positioned: .above, relativeTo: localInlineSlotView) + hostedInspectorSideDockConstraints = [ + containerView.topAnchor.constraint(equalTo: topAnchor), + containerView.bottomAnchor.constraint(equalTo: bottomAnchor), + containerView.leadingAnchor.constraint(equalTo: leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: trailingAnchor), + ] + NSLayoutConstraint.activate(hostedInspectorSideDockConstraints) + hostedInspectorSideDockContainerView = containerView + return containerView + } + + private func moveHostedInspectorSubviewIfNeeded(_ view: NSView, to container: NSView) { + guard view.superview !== container else { return } + let frameInWindow = view.superview?.convert(view.frame, to: nil) ?? convert(view.frame, to: nil) + view.removeFromSuperview() + container.addSubview(view, positioned: .above, relativeTo: nil) + view.frame = container.convert(frameInWindow, from: nil) + } + + private func isHostedInspectorSideDockActive() -> Bool { + guard let hostedInspectorSideDockContainerView, + let hostedInspectorSideDockPageView, + let hostedInspectorSideDockInspectorView else { + return false + } + return hostedInspectorSideDockPageView.superview === hostedInspectorSideDockContainerView && + hostedInspectorSideDockInspectorView.superview === hostedInspectorSideDockContainerView + } + + private func isHostedInspectorSideDockHit(_ hit: HostedInspectorDividerHit) -> Bool { + guard let hostedInspectorSideDockContainerView else { return false } + return hit.containerView === hostedInspectorSideDockContainerView + } + + private func activateHostedInspectorSideDockIfNeeded(using hit: HostedInspectorDividerHit) { + let containerView = ensureHostedInspectorSideDockContainerView() + moveHostedInspectorSubviewIfNeeded(hit.pageView, to: containerView) + moveHostedInspectorSubviewIfNeeded(hit.inspectorView, to: containerView) + hostedInspectorSideDockPageView = hit.pageView + hostedInspectorSideDockInspectorView = hit.inspectorView + hostedInspectorSideDockDockSide = hit.dockSide + layoutHostedInspectorSideDockIfNeeded(reason: "sideDock.activate") + } + + private func deactivateHostedInspectorSideDockIfNeeded(reparentTo slotView: WindowBrowserSlotView?) { + guard let slotView, + let pageView = hostedInspectorSideDockPageView, + let inspectorView = hostedInspectorSideDockInspectorView else { + hostedInspectorSideDockPageView = nil + hostedInspectorSideDockInspectorView = nil + hostedInspectorSideDockDockSide = nil + hostedInspectorSideDockContainerView?.isHidden = true + return + } + + moveHostedInspectorSubviewIfNeeded(pageView, to: slotView) + moveHostedInspectorSubviewIfNeeded(inspectorView, to: slotView) + hostedInspectorSideDockPageView = nil + hostedInspectorSideDockInspectorView = nil + hostedInspectorSideDockDockSide = nil + hostedInspectorSideDockContainerView?.isHidden = true + } + + private func layoutHostedInspectorSideDockIfNeeded(reason: String) { + guard let containerView = hostedInspectorSideDockContainerView, + let pageView = hostedInspectorSideDockPageView, + let inspectorView = hostedInspectorSideDockInspectorView, + let dockSide = hostedInspectorSideDockDockSide else { + return + } + let preferredWidth = resolvedPreferredHostedInspectorWidth(in: containerView.bounds) ?? max(0, inspectorView.frame.width) + _ = applyHostedInspectorDividerWidth( + preferredWidth, + to: HostedInspectorDividerHit( + containerView: containerView, + pageView: pageView, + inspectorView: inspectorView, + dockSide: dockSide + ), + reason: reason + ) + } + + fileprivate func scheduleHostedInspectorDockConfigurationSync(reason: String) { + hostedInspectorDockConfigurationSyncWorkItem?.cancel() + guard hostedInspectorFrontendWebView != nil else { return } + let workItem = DispatchWorkItem { [weak self] in + self?.syncHostedInspectorDockConfiguration(reason: reason) + } + hostedInspectorDockConfigurationSyncWorkItem = workItem + DispatchQueue.main.async(execute: workItem) + } + + private func syncHostedInspectorDockConfiguration(reason: String) { + hostedInspectorDockConfigurationSyncWorkItem = nil + guard let hostedInspectorFrontendWebView else { return } + hostedInspectorFrontendWebView.evaluateJavaScript( + "typeof WI === 'undefined' ? null : WI.dockConfiguration" + ) { [weak self] result, _ in + self?.applyHostedInspectorDockConfiguration(result as? String, reason: reason) + } + } + + private func applyHostedInspectorDockConfiguration(_ dockConfiguration: String?, reason: String) { + switch dockConfiguration { + case "left": + hostedInspectorSideDockDockSide = .leading + if isHostedInspectorSideDockActive() { + layoutHostedInspectorSideDockIfNeeded(reason: "\(reason).dockLeft") + } else if let slotView = localInlineSlotView, + let hit = hostedInspectorDividerCandidate(in: slotView), + hit.dockSide == .leading { + activateHostedInspectorSideDockIfNeeded(using: hit) + } + case "right": + hostedInspectorSideDockDockSide = .trailing + if isHostedInspectorSideDockActive() { + layoutHostedInspectorSideDockIfNeeded(reason: "\(reason).dockRight") + } else if let slotView = localInlineSlotView, + let hit = hostedInspectorDividerCandidate(in: slotView), + hit.dockSide == .trailing { + activateHostedInspectorSideDockIfNeeded(using: hit) + } + default: + if isHostedInspectorSideDockActive() { + deactivateHostedInspectorSideDockIfNeeded(reparentTo: localInlineSlotView) + if dockConfiguration == "bottom" { + hostedInspectorFrontendWebView?.evaluateJavaScript( + "typeof WI !== 'undefined' ? WI._dockBottom() : null", + completionHandler: nil + ) + } + } + } + } + override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if window == nil { clearActiveDividerCursor(restoreArrow: false) } else { scheduleHostedInspectorDividerReapply(reason: "viewDidMoveToWindow") + scheduleHostedInspectorDockConfigurationSync(reason: "viewDidMoveToWindow") } window?.invalidateCursorRects(for: self) onDidMoveToWindow?() @@ -3895,6 +4092,7 @@ struct WebViewRepresentable: NSViewRepresentable { override func viewDidMoveToSuperview() { super.viewDidMoveToSuperview() scheduleHostedInspectorDividerReapply(reason: "viewDidMoveToSuperview") + scheduleHostedInspectorDockConfigurationSync(reason: "viewDidMoveToSuperview") notifyGeometryChangedIfNeeded() #if DEBUG debugLogHostedInspectorLayoutIfNeeded(reason: "viewDidMoveToSuperview") @@ -3905,6 +4103,11 @@ struct WebViewRepresentable: NSViewRepresentable { super.layout() if let previousSize = lastHostedInspectorLayoutBoundsSize, Self.sizeApproximatelyEqual(previousSize, bounds.size, epsilon: 0.5) { + if isHostedInspectorSideDockActive() { + layoutHostedInspectorSideDockIfNeeded(reason: "host.layout.sideDock.sameSize") + } else if !isHostedInspectorDividerDragActive && !hasStoredHostedInspectorWidthPreference { + captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout.sameSize") + } notifyGeometryChangedIfNeeded() #if DEBUG debugLogHostedInspectorLayoutIfNeeded(reason: "layout") @@ -3912,7 +4115,12 @@ struct WebViewRepresentable: NSViewRepresentable { return } lastHostedInspectorLayoutBoundsSize = bounds.size - captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout") + if isHostedInspectorSideDockActive() { + layoutHostedInspectorSideDockIfNeeded(reason: "host.layout.sideDock") + } else if !hasStoredHostedInspectorWidthPreference { + captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout") + } + scheduleHostedInspectorDockConfigurationSync(reason: "layout") notifyGeometryChangedIfNeeded() #if DEBUG debugLogHostedInspectorLayoutIfNeeded(reason: "layout") @@ -3921,6 +4129,9 @@ struct WebViewRepresentable: NSViewRepresentable { override func setFrameOrigin(_ newOrigin: NSPoint) { super.setFrameOrigin(newOrigin) + if isHostedInspectorSideDockActive() { + layoutHostedInspectorSideDockIfNeeded(reason: "setFrameOrigin.sideDock") + } window?.invalidateCursorRects(for: self) notifyGeometryChangedIfNeeded() #if DEBUG @@ -3930,6 +4141,9 @@ struct WebViewRepresentable: NSViewRepresentable { override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) + if isHostedInspectorSideDockActive() { + layoutHostedInspectorSideDockIfNeeded(reason: "setFrameSize.sideDock") + } window?.invalidateCursorRects(for: self) notifyGeometryChangedIfNeeded() #if DEBUG @@ -3986,16 +4200,26 @@ struct WebViewRepresentable: NSViewRepresentable { return nil } if let hostedInspectorHit { + let isSideDockHit = isHostedInspectorSideDockHit(hostedInspectorHit) if let nativeHit = nativeHostedInspectorHit(at: point, hostedInspectorHit: hostedInspectorHit) { #if DEBUG debugLogHitTest(stage: "hitTest.hostedInspectorNative", point: point, passThrough: false, hitView: nativeHit) #endif - return nativeHit + if !isSideDockHit || + (nativeHit !== hostedInspectorHit.inspectorView && + !hostedInspectorHit.inspectorView.isDescendant(of: nativeHit)) { + return nativeHit + } } #if DEBUG - debugLogHitTest(stage: "hitTest.hostedInspectorFallback", point: point, passThrough: false, hitView: hostedInspectorHit.inspectorView) + debugLogHitTest( + stage: isSideDockHit ? "hitTest.hostedInspectorManual" : "hitTest.hostedInspectorFallback", + point: point, + passThrough: false, + hitView: hostedInspectorHit.inspectorView + ) #endif - return hostedInspectorHit.inspectorView + return isSideDockHit ? self : hostedInspectorHit.inspectorView } let hit = super.hitTest(point) #if DEBUG @@ -4005,17 +4229,106 @@ struct WebViewRepresentable: NSViewRepresentable { } override func mouseDown(with event: NSEvent) { - super.mouseDown(with: event) + let point = convert(event.locationInWindow, from: nil) + guard let hostedInspectorHit = hostedInspectorDividerHit(at: point), + isHostedInspectorSideDockHit(hostedInspectorHit) else { + super.mouseDown(with: event) + return + } + + hostedInspectorReapplyWorkItem?.cancel() + isHostedInspectorDividerDragActive = true + hostedInspectorDividerDrag = HostedInspectorDividerDragState( + containerView: hostedInspectorHit.containerView, + pageView: hostedInspectorHit.pageView, + inspectorView: hostedInspectorHit.inspectorView, + dockSide: hostedInspectorHit.dockSide, + initialWindowX: event.locationInWindow.x, + initialPageFrame: hostedInspectorHit.pageView.frame, + initialInspectorFrame: hostedInspectorHit.inspectorView.frame + ) +#if DEBUG + debugLogHostedInspectorFrames(stage: "drag.start", point: point, hit: hostedInspectorHit) +#endif } override func mouseDragged(with event: NSEvent) { - super.mouseDragged(with: event) + guard let dragState = hostedInspectorDividerDrag else { + super.mouseDragged(with: event) + return + } + + let containerBounds = dragState.containerView.bounds + let minimumInspectorWidth = Self.minimumHostedInspectorWidth + let initialDividerX = dragState.dockSide.dividerX( + pageFrame: dragState.initialPageFrame, + inspectorFrame: dragState.initialInspectorFrame + ) + let proposedDividerX = initialDividerX + (event.locationInWindow.x - dragState.initialWindowX) + let clampedDividerX = dragState.dockSide.clampedDividerX( + proposedDividerX, + containerBounds: containerBounds, + pageFrame: dragState.initialPageFrame, + minimumInspectorWidth: minimumInspectorWidth + ) + let inspectorWidth = dragState.dockSide.inspectorWidth( + forDividerX: clampedDividerX, + in: containerBounds + ) + recordPreferredHostedInspectorWidth(inspectorWidth, containerBounds: containerBounds) + _ = applyHostedInspectorDividerWidth( + inspectorWidth, + to: HostedInspectorDividerHit( + containerView: dragState.containerView, + pageView: dragState.pageView, + inspectorView: dragState.inspectorView, + dockSide: dragState.dockSide + ), + reason: "drag" + ) +#if DEBUG + debugLogHostedInspectorFrames( + stage: "drag.update", + point: convert(event.locationInWindow, from: nil), + hit: HostedInspectorDividerHit( + containerView: dragState.containerView, + pageView: dragState.pageView, + inspectorView: dragState.inspectorView, + dockSide: dragState.dockSide + ) + ) +#endif + updateDividerCursor( + at: convert(event.locationInWindow, from: nil), + hostedInspectorHit: HostedInspectorDividerHit( + containerView: dragState.containerView, + pageView: dragState.pageView, + inspectorView: dragState.inspectorView, + dockSide: dragState.dockSide + ) + ) } override func mouseUp(with event: NSEvent) { + let finalDragState = hostedInspectorDividerDrag hostedInspectorDividerDrag = nil isHostedInspectorDividerDragActive = false updateDividerCursor(at: convert(event.locationInWindow, from: nil)) + if let finalDragState { +#if DEBUG + debugLogHostedInspectorFrames( + stage: "drag.end", + point: convert(event.locationInWindow, from: nil), + hit: HostedInspectorDividerHit( + containerView: finalDragState.containerView, + pageView: finalDragState.pageView, + inspectorView: finalDragState.inspectorView, + dockSide: finalDragState.dockSide + ) + ) +#endif + layoutHostedInspectorSideDockIfNeeded(reason: "drag.end") + } super.mouseUp(with: event) } @@ -4094,11 +4407,19 @@ struct WebViewRepresentable: NSViewRepresentable { } private func hostedInspectorDividerCandidate() -> HostedInspectorDividerHit? { - let inspectorCandidates = Self.visibleDescendants(in: self) + hostedInspectorDividerCandidate(in: self) + } + + private func hostedInspectorDividerCandidate(in root: NSView) -> HostedInspectorDividerHit? { + if let preferredHit = hostedInspectorDividerCandidateUsingKnownWebViews(in: root) { + return preferredHit + } + + let inspectorCandidates = Self.visibleDescendants(in: root) .filter { Self.isVisibleHostedInspectorCandidate($0) && Self.isInspectorView($0) } .sorted { lhs, rhs in - let lhsFrame = convert(lhs.bounds, from: lhs) - let rhsFrame = convert(rhs.bounds, from: rhs) + let lhsFrame = root.convert(lhs.bounds, from: lhs) + let rhsFrame = root.convert(rhs.bounds, from: rhs) return lhsFrame.minX < rhsFrame.minX } @@ -4106,7 +4427,7 @@ struct WebViewRepresentable: NSViewRepresentable { var bestScore = -CGFloat.greatestFiniteMagnitude for inspectorCandidate in inspectorCandidates { - guard let candidate = hostedInspectorDividerCandidate(startingAt: inspectorCandidate) else { + guard let candidate = hostedInspectorDividerCandidate(in: root, startingAt: inspectorCandidate) else { continue } let score = hostedInspectorDividerCandidateScore(candidate) @@ -4119,6 +4440,59 @@ struct WebViewRepresentable: NSViewRepresentable { return bestHit } + private func hostedInspectorDividerCandidateUsingKnownWebViews(in root: NSView) -> HostedInspectorDividerHit? { + guard let pageLeaf = hostedWebView, + let inspectorLeaf = hostedInspectorFrontendWebView, + pageLeaf.isDescendant(of: root), + inspectorLeaf.isDescendant(of: root), + Self.isVisibleHostedInspectorCandidate(inspectorLeaf) else { + return nil + } + return hostedInspectorDividerCandidate( + in: root, + pageLeaf: pageLeaf, + inspectorLeaf: inspectorLeaf + ) + } + + private func hostedInspectorDividerCandidate( + in root: NSView, + pageLeaf: NSView, + inspectorLeaf: NSView + ) -> HostedInspectorDividerHit? { + var currentInspector: NSView? = inspectorLeaf + + while let inspectorView = currentInspector, inspectorView !== root { + guard let containerView = inspectorView.superview else { break } + guard containerView === root || containerView.isDescendant(of: root) else { + currentInspector = containerView + continue + } + guard let pageView = Self.directChild(of: containerView, containing: pageLeaf) else { + currentInspector = containerView + continue + } + guard pageView !== inspectorView, + Self.isVisibleHostedInspectorSiblingCandidate(pageView), + Self.verticalOverlap(between: pageView.frame, and: inspectorView.frame) > 8, + let dockSide = HostedInspectorDockSide.resolve( + pageFrame: pageView.frame, + inspectorFrame: inspectorView.frame + ) else { + currentInspector = containerView + continue + } + return HostedInspectorDividerHit( + containerView: containerView, + pageView: pageView, + inspectorView: inspectorView, + dockSide: dockSide + ) + } + + return nil + } + private func hostedInspectorDividerHitRect(for hit: HostedInspectorDividerHit) -> NSRect { let pageFrame = convert(hit.pageView.bounds, from: hit.pageView) let inspectorFrame = convert(hit.inspectorView.bounds, from: hit.inspectorView) @@ -4130,11 +4504,11 @@ struct WebViewRepresentable: NSViewRepresentable { ) } - private func hostedInspectorDividerCandidate(startingAt inspectorLeaf: NSView) -> HostedInspectorDividerHit? { + private func hostedInspectorDividerCandidate(in root: NSView, startingAt inspectorLeaf: NSView) -> HostedInspectorDividerHit? { var current: NSView? = inspectorLeaf var bestHit: HostedInspectorDividerHit? - while let inspectorView = current, inspectorView !== self { + while let inspectorView = current, inspectorView !== root { guard let containerView = inspectorView.superview else { break } let pageCandidates = containerView.subviews.compactMap { candidate -> (view: NSView, dockSide: HostedInspectorDockSide)? in @@ -4189,7 +4563,11 @@ struct WebViewRepresentable: NSViewRepresentable { let workItem = DispatchWorkItem { [weak self] in guard let self else { return } self.hostedInspectorReapplyWorkItem = nil - self.captureHostedInspectorPreferredWidthFromCurrentLayout(reason: reason) + if self.isHostedInspectorSideDockActive() { + self.reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: reason) + } else if !self.hasStoredHostedInspectorWidthPreference { + self.captureHostedInspectorPreferredWidthFromCurrentLayout(reason: reason) + } } hostedInspectorReapplyWorkItem = workItem DispatchQueue.main.async(execute: workItem) @@ -4244,6 +4622,18 @@ struct WebViewRepresentable: NSViewRepresentable { ) } + private func reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: String) { + guard !isApplyingHostedInspectorLayout else { return } + guard let hit = hostedInspectorDividerCandidate() else { return } + guard isHostedInspectorSideDockHit(hit) else { return } + guard let preferredWidth = resolvedPreferredHostedInspectorWidth(in: hit.containerView.bounds) else { + return + } + let currentInspectorWidth = max(0, hit.inspectorView.frame.width) + guard abs(currentInspectorWidth - preferredWidth) > 0.5 else { return } + _ = applyHostedInspectorDividerWidth(preferredWidth, to: hit, reason: reason) + } + @discardableResult private func applyHostedInspectorDividerWidth( _ preferredWidth: CGFloat, @@ -4302,6 +4692,17 @@ struct WebViewRepresentable: NSViewRepresentable { return descendants } + private static func directChild(of container: NSView, containing descendant: NSView) -> NSView? { + var current: NSView? = descendant + var directChild: NSView? + while let view = current, view !== container { + directChild = view + current = view.superview + } + guard current === container else { return nil } + return directChild + } + fileprivate static func isInspectorView(_ view: NSView) -> Bool { String(describing: type(of: view)).contains("WKInspector") } @@ -4509,9 +4910,7 @@ struct WebViewRepresentable: NSViewRepresentable { private func updateUsingLocalInlineHosting(_ nsView: NSView, context: Context, webView: WKWebView) -> Bool { guard let host = nsView as? HostContainerView else { return false } let slotView = host.ensureLocalInlineSlotView() - let isAlreadyInLocalHost = - webView.superview === slotView || - (webView.superview?.isDescendant(of: slotView) ?? false) + let isAlreadyInLocalHost = host.containsManagedLocalInlineContent(webView) let didAttachWebViewToLocalHost = !isAlreadyInLocalHost let coordinator = context.coordinator @@ -4532,7 +4931,7 @@ struct WebViewRepresentable: NSViewRepresentable { let shouldPreserveExistingExternalLocalHost = host.window == nil && webView.superview != nil && - webView.superview !== slotView + !host.containsManagedLocalInlineContent(webView) if shouldPreserveExistingExternalLocalHost { // Split zoom can instantiate a replacement local host before it joins a window. // Never let that off-window host steal the live page + inspector hierarchy away @@ -4557,6 +4956,12 @@ struct WebViewRepresentable: NSViewRepresentable { return false } + let preferredAttachedWidthState = panel.preferredAttachedDeveloperToolsWidthState() + host.setPreferredHostedInspectorWidth( + width: preferredAttachedWidthState.width, + widthFraction: preferredAttachedWidthState.widthFraction + ) + host.setHostedInspectorFrontendWebView(webView.cmuxInspectorFrontendWebView()) host.onPreferredHostedInspectorWidthChanged = { [weak browserPanel = panel] width, _ in guard let browserPanel else { return } browserPanel.recordPreferredAttachedDeveloperToolsWidth( @@ -4566,6 +4971,7 @@ struct WebViewRepresentable: NSViewRepresentable { } slotView.onHostedInspectorLayout = { [weak host] _ in host?.scheduleHostedInspectorDividerReapply(reason: "slot.layout") + host?.scheduleHostedInspectorDockConfigurationSync(reason: "slot.layout") } if didAttachWebViewToLocalHost { @@ -4582,7 +4988,10 @@ struct WebViewRepresentable: NSViewRepresentable { } slotView.isHidden = false - host.pinHostedWebView(webView, in: slotView) + host.pinHostedWebView( + webView, + in: host.currentHostedWebViewContainer(preferredSlotView: slotView) + ) coordinator.lastPortalHostId = nil coordinator.lastSynchronizedHostGeometryRevision = 0 if didAttachWebViewToLocalHost { @@ -4592,6 +5001,13 @@ struct WebViewRepresentable: NSViewRepresentable { slotView.layoutSubtreeIfNeeded() host.layoutSubtreeIfNeeded() host.scheduleHostedInspectorDividerReapply(reason: "localInline.update.sync") + DispatchQueue.main.async { [weak host, weak webView] in + guard let host, let webView else { return } + host.setHostedInspectorFrontendWebView(webView.cmuxInspectorFrontendWebView()) + host.scheduleHostedInspectorDockConfigurationSync(reason: "localInline.update.async") + } + } else { + host.scheduleHostedInspectorDockConfigurationSync(reason: "localInline.update") } #if DEBUG @@ -4608,6 +5024,7 @@ struct WebViewRepresentable: NSViewRepresentable { private func updateUsingWindowPortal(_ nsView: NSView, context: Context, webView: WKWebView) -> Bool { guard let host = nsView as? HostContainerView else { return false } + host.prepareForWindowPortalHosting() host.setLocalInlineSlotHidden(true) host.releaseHostedWebViewConstraints()