diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index 200a9ef0..9d591ce3 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -2229,6 +2229,71 @@ final class WindowBrowserPortal: NSObject { frame.maxY > bounds.maxY + epsilon } + private static func hasVisibleInspectorDescendant(in root: NSView) -> Bool { + var stack: [NSView] = [root] + while let current = stack.popLast() { + if current !== root { + let className = String(describing: type(of: current)) + if className.contains("WKInspector"), + !current.isHidden, + current.alphaValue > 0, + current.frame.width > 1, + current.frame.height > 1 { + return true + } + } + stack.append(contentsOf: current.subviews) + } + return false + } + + private static func inferredBottomDockedInspectorFrame( + in containerView: NSView, + primaryWebView: WKWebView, + epsilon: CGFloat = 1 + ) -> NSRect? { + let pageFrame = primaryWebView.frame + let containerBounds = containerView.bounds + + let candidates = containerView.subviews.compactMap { candidate -> NSRect? in + guard candidate !== primaryWebView else { return nil } + guard hasVisibleInspectorDescendant(in: candidate) else { return nil } + + let frame = candidate.frame + guard frame.width > 1, frame.height > 1 else { return nil } + let overlapWidth = min(pageFrame.maxX, frame.maxX) - max(pageFrame.minX, frame.minX) + guard overlapWidth > min(pageFrame.width, frame.width) * 0.7 else { return nil } + guard frame.minY <= containerBounds.minY + epsilon else { return nil } + guard frame.maxY <= pageFrame.minY + epsilon else { return nil } + return frame + } + + return candidates.max(by: { $0.height < $1.height }) + } + + private static func repairedBottomDockedPageFrame( + in containerView: NSView, + primaryWebView: WKWebView, + epsilon: CGFloat = 0.5 + ) -> NSRect? { + let pageFrame = primaryWebView.frame + let containerBounds = containerView.bounds + guard frameExtendsOutsideBounds(pageFrame, bounds: containerBounds, epsilon: epsilon), + let inspectorFrame = inferredBottomDockedInspectorFrame( + in: containerView, + primaryWebView: primaryWebView + ) else { + return nil + } + + return NSRect( + x: containerBounds.minX, + y: inspectorFrame.maxY, + width: containerBounds.width, + height: max(0, containerBounds.maxY - inspectorFrame.maxY) + ) + } + #if DEBUG private static func inspectorSubviewCount(in root: NSView) -> Int { var stack: [NSView] = [root] @@ -3073,7 +3138,30 @@ final class WindowBrowserPortal: NSObject { #if DEBUG let inspectorSubviews = Self.inspectorSubviewCount(in: containerView) #endif - if containerOwnsWebView && Self.frameExtendsOutsideBounds(preNormalizeWebFrame, bounds: containerBounds) { + if containerOwnsWebView, + let repairedBottomDockFrame = Self.repairedBottomDockedPageFrame( + in: containerView, + primaryWebView: webView + ) { + let oldWebFrame = preNormalizeWebFrame + CATransaction.begin() + CATransaction.setDisableActions(true) + webView.frame = repairedBottomDockFrame + CATransaction.commit() +#if DEBUG + dlog( + "browser.portal.webframe.bottomDockRepair web=\(browserPortalDebugToken(webView)) " + + "container=\(browserPortalDebugToken(containerView)) old=\(browserPortalDebugFrame(oldWebFrame)) " + + "new=\(browserPortalDebugFrame(repairedBottomDockFrame)) bounds=\(browserPortalDebugFrame(containerBounds)) " + + "inspectorHApprox=\(String(format: "%.1f", inspectorHeightApprox)) " + + "inspectorInsets=\(String(format: "%.1f", inspectorHeightFromInsets)) " + + "inspectorOverflow=\(String(format: "%.1f", inspectorHeightFromOverflow)) " + + "inspectorSubviews=\(inspectorSubviews) " + + "source=\(source)" + ) +#endif + refreshReasons.append("webFrameBottomDock") + } else if containerOwnsWebView && Self.frameExtendsOutsideBounds(preNormalizeWebFrame, bounds: containerBounds) { let oldWebFrame = preNormalizeWebFrame CATransaction.begin() CATransaction.setDisableActions(true)