Stabilize docked devtools side resize

This commit is contained in:
austinpower1258 2026-03-11 12:26:19 -07:00
parent 8b66628b19
commit 06c5cac4df
2 changed files with 449 additions and 22 deletions

View file

@ -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 {

View file

@ -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()