wip
This commit is contained in:
parent
6849b83f8d
commit
7d5d4d718d
4 changed files with 602 additions and 62 deletions
|
|
@ -126,13 +126,11 @@ enum HostedInspectorDockSide {
|
|||
inspectorFrame: NSRect,
|
||||
expansion: CGFloat
|
||||
) -> NSRect {
|
||||
let minY = max(bounds.minY, min(pageFrame.minY, inspectorFrame.minY))
|
||||
let maxY = min(bounds.maxY, max(pageFrame.maxY, inspectorFrame.maxY))
|
||||
return NSRect(
|
||||
x: dividerX(pageFrame: pageFrame, inspectorFrame: inspectorFrame) - expansion,
|
||||
y: minY,
|
||||
y: bounds.minY,
|
||||
width: expansion * 2,
|
||||
height: max(0, maxY - minY)
|
||||
height: max(0, bounds.height)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -168,35 +166,54 @@ enum HostedInspectorDockSide {
|
|||
in containerBounds: NSRect,
|
||||
pageFrame: NSRect,
|
||||
inspectorFrame: NSRect,
|
||||
minimumInspectorWidth _: CGFloat
|
||||
minimumInspectorWidth: CGFloat
|
||||
) -> (pageFrame: NSRect, inspectorFrame: NSRect) {
|
||||
let normalizedMinY = containerBounds.minY
|
||||
let normalizedHeight = max(0, containerBounds.height)
|
||||
|
||||
switch self {
|
||||
case .leading:
|
||||
let maximumInspectorWidth = max(0, containerBounds.width)
|
||||
let clampedInspectorWidth = max(0, min(maximumInspectorWidth, preferredWidth))
|
||||
let clampedMinimumInspectorWidth = min(maximumInspectorWidth, max(0, minimumInspectorWidth))
|
||||
let clampedInspectorWidth = min(
|
||||
maximumInspectorWidth,
|
||||
max(clampedMinimumInspectorWidth, preferredWidth)
|
||||
)
|
||||
let dividerX = min(containerBounds.maxX, containerBounds.minX + clampedInspectorWidth)
|
||||
|
||||
var nextPageFrame = pageFrame
|
||||
nextPageFrame.origin.x = dividerX
|
||||
nextPageFrame.origin.y = normalizedMinY
|
||||
nextPageFrame.size.width = max(0, containerBounds.maxX - dividerX)
|
||||
nextPageFrame.size.height = normalizedHeight
|
||||
|
||||
var nextInspectorFrame = inspectorFrame
|
||||
nextInspectorFrame.origin.x = containerBounds.minX
|
||||
nextInspectorFrame.origin.y = normalizedMinY
|
||||
nextInspectorFrame.size.width = max(0, dividerX - containerBounds.minX)
|
||||
nextInspectorFrame.size.height = normalizedHeight
|
||||
return (pageFrame: nextPageFrame, inspectorFrame: nextInspectorFrame)
|
||||
|
||||
case .trailing:
|
||||
let maximumInspectorWidth = max(0, containerBounds.width)
|
||||
let clampedInspectorWidth = max(0, min(maximumInspectorWidth, preferredWidth))
|
||||
let clampedMinimumInspectorWidth = min(maximumInspectorWidth, max(0, minimumInspectorWidth))
|
||||
let clampedInspectorWidth = min(
|
||||
maximumInspectorWidth,
|
||||
max(clampedMinimumInspectorWidth, preferredWidth)
|
||||
)
|
||||
let dividerX = max(containerBounds.minX, containerBounds.maxX - clampedInspectorWidth)
|
||||
|
||||
var nextPageFrame = pageFrame
|
||||
nextPageFrame.origin.x = containerBounds.minX
|
||||
nextPageFrame.origin.y = normalizedMinY
|
||||
nextPageFrame.size.width = max(0, dividerX - containerBounds.minX)
|
||||
nextPageFrame.size.height = normalizedHeight
|
||||
|
||||
var nextInspectorFrame = inspectorFrame
|
||||
nextInspectorFrame.origin.x = dividerX
|
||||
nextInspectorFrame.origin.y = normalizedMinY
|
||||
nextInspectorFrame.size.width = max(0, containerBounds.maxX - dividerX)
|
||||
nextInspectorFrame.size.height = normalizedHeight
|
||||
return (pageFrame: nextPageFrame, inspectorFrame: nextInspectorFrame)
|
||||
}
|
||||
}
|
||||
|
|
@ -572,6 +589,7 @@ final class WindowBrowserHostView: NSView {
|
|||
inspectorView: dragState.inspectorView,
|
||||
dockSide: dragState.dockSide
|
||||
),
|
||||
minimumInspectorWidth: Self.minimumHostedInspectorWidth,
|
||||
reason: "drag"
|
||||
)
|
||||
updateDividerCursor(
|
||||
|
|
@ -946,7 +964,12 @@ final class WindowBrowserHostView: NSView {
|
|||
guard let hit = hostedInspectorDividerCandidate(in: slot) else { return false }
|
||||
let oldPageFrame = hit.pageView.frame
|
||||
let oldInspectorFrame = hit.inspectorView.frame
|
||||
_ = applyHostedInspectorDividerWidth(preferredWidth, to: hit, reason: reason)
|
||||
_ = applyHostedInspectorDividerWidth(
|
||||
preferredWidth,
|
||||
to: hit,
|
||||
minimumInspectorWidth: Self.minimumHostedInspectorWidth,
|
||||
reason: reason
|
||||
)
|
||||
return !Self.rectApproximatelyEqual(oldPageFrame, hit.pageView.frame, epsilon: 0.5) ||
|
||||
!Self.rectApproximatelyEqual(oldInspectorFrame, hit.inspectorView.frame, epsilon: 0.5)
|
||||
}
|
||||
|
|
@ -955,6 +978,7 @@ final class WindowBrowserHostView: NSView {
|
|||
private func applyHostedInspectorDividerWidth(
|
||||
_ preferredWidth: CGFloat,
|
||||
to hit: HostedInspectorDividerHit,
|
||||
minimumInspectorWidth: CGFloat,
|
||||
reason: String
|
||||
) -> (pageFrame: NSRect, inspectorFrame: NSRect) {
|
||||
let containerBounds = hit.containerView.bounds
|
||||
|
|
@ -963,7 +987,7 @@ final class WindowBrowserHostView: NSView {
|
|||
in: containerBounds,
|
||||
pageFrame: hit.pageView.frame,
|
||||
inspectorFrame: hit.inspectorView.frame,
|
||||
minimumInspectorWidth: 0
|
||||
minimumInspectorWidth: minimumInspectorWidth
|
||||
)
|
||||
let pageFrame = nextFrames.pageFrame
|
||||
let inspectorFrame = nextFrames.inspectorFrame
|
||||
|
|
@ -1742,8 +1766,9 @@ final class WindowBrowserSlotView: NSView {
|
|||
func pinHostedWebView(_ webView: WKWebView) {
|
||||
guard webView.superview === self else { return }
|
||||
|
||||
let hasCompanionWKSubviews = Self.hasWebKitCompanionSubview(in: self, primaryWebView: webView)
|
||||
let needsPlainWebViewFrameReset =
|
||||
!Self.hasWebKitCompanionSubview(in: self, primaryWebView: webView) &&
|
||||
!hasCompanionWKSubviews &&
|
||||
Self.frameDiffersFromBounds(webView.frame, bounds: bounds)
|
||||
let needsFrameHosting =
|
||||
hostedWebView !== webView ||
|
||||
|
|
@ -1765,7 +1790,9 @@ final class WindowBrowserSlotView: NSView {
|
|||
// WebKit-managed split frame when docked DevTools siblings are present.
|
||||
webView.translatesAutoresizingMaskIntoConstraints = true
|
||||
webView.autoresizingMask = [.width, .height]
|
||||
webView.frame = bounds
|
||||
if !hasCompanionWKSubviews {
|
||||
webView.frame = bounds
|
||||
}
|
||||
needsLayout = true
|
||||
layoutSubtreeIfNeeded()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1775,6 +1775,10 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
private let developerToolsRestoreRetryMaxAttempts: Int = 40
|
||||
private let developerToolsDetachedOpenGracePeriod: TimeInterval = 0.35
|
||||
private var developerToolsDetachedOpenGraceDeadline: Date?
|
||||
private var developerToolsTransitionTargetVisible: Bool?
|
||||
private var pendingDeveloperToolsTransitionTargetVisible: Bool?
|
||||
private var developerToolsTransitionSettleWorkItem: DispatchWorkItem?
|
||||
private let developerToolsTransitionSettleDelay: TimeInterval = 0.15
|
||||
private var detachedDeveloperToolsWindowCloseObserver: NSObjectProtocol?
|
||||
private var preferredAttachedDeveloperToolsWidth: CGFloat?
|
||||
private var preferredAttachedDeveloperToolsWidthFraction: CGFloat?
|
||||
|
|
@ -2698,6 +2702,8 @@ final class BrowserPanel: Panel, ObservableObject {
|
|||
deinit {
|
||||
developerToolsRestoreRetryWorkItem?.cancel()
|
||||
developerToolsRestoreRetryWorkItem = nil
|
||||
developerToolsTransitionSettleWorkItem?.cancel()
|
||||
developerToolsTransitionSettleWorkItem = nil
|
||||
if let detachedDeveloperToolsWindowCloseObserver {
|
||||
NotificationCenter.default.removeObserver(detachedDeveloperToolsWindowCloseObserver)
|
||||
}
|
||||
|
|
@ -3002,29 +3008,97 @@ extension BrowserPanel {
|
|||
return false
|
||||
}
|
||||
|
||||
private var isDeveloperToolsTransitionInFlight: Bool {
|
||||
developerToolsTransitionSettleWorkItem != nil
|
||||
}
|
||||
|
||||
private func effectiveDeveloperToolsVisibilityIntent() -> Bool {
|
||||
if let pendingDeveloperToolsTransitionTargetVisible {
|
||||
return pendingDeveloperToolsTransitionTargetVisible
|
||||
}
|
||||
if let developerToolsTransitionTargetVisible {
|
||||
return developerToolsTransitionTargetVisible
|
||||
}
|
||||
return isDeveloperToolsVisible()
|
||||
}
|
||||
|
||||
private func scheduleDeveloperToolsTransitionSettle(source: String) {
|
||||
developerToolsTransitionSettleWorkItem?.cancel()
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
self?.developerToolsTransitionSettleWorkItem = nil
|
||||
self?.finishDeveloperToolsTransition(source: source)
|
||||
}
|
||||
developerToolsTransitionSettleWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + developerToolsTransitionSettleDelay, execute: workItem)
|
||||
}
|
||||
|
||||
private func finishDeveloperToolsTransition(source: String) {
|
||||
let pendingTargetVisible = pendingDeveloperToolsTransitionTargetVisible
|
||||
pendingDeveloperToolsTransitionTargetVisible = nil
|
||||
developerToolsTransitionTargetVisible = nil
|
||||
|
||||
guard let pendingTargetVisible else { return }
|
||||
guard pendingTargetVisible != isDeveloperToolsVisible() else { return }
|
||||
_ = performDeveloperToolsVisibilityTransition(to: pendingTargetVisible, source: "\(source).queued")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func toggleDeveloperTools() -> Bool {
|
||||
private func enqueueDeveloperToolsVisibilityTransition(
|
||||
to targetVisible: Bool,
|
||||
source: String
|
||||
) -> Bool {
|
||||
if isDeveloperToolsTransitionInFlight {
|
||||
pendingDeveloperToolsTransitionTargetVisible = targetVisible
|
||||
preferredDeveloperToolsVisible = targetVisible
|
||||
if !targetVisible {
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
forceDeveloperToolsRefreshOnNextAttach = false
|
||||
cancelDeveloperToolsRestoreRetry()
|
||||
}
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.devtools toggle.begin panel=\(id.uuidString.prefix(5)) " +
|
||||
"\(debugDeveloperToolsStateSummary()) \(debugDeveloperToolsGeometrySummary())"
|
||||
)
|
||||
dlog(
|
||||
"browser.devtools transition.queue panel=\(id.uuidString.prefix(5)) " +
|
||||
"source=\(source) target=\(targetVisible ? 1 : 0) \(debugDeveloperToolsStateSummary())"
|
||||
)
|
||||
#endif
|
||||
return true
|
||||
}
|
||||
|
||||
return performDeveloperToolsVisibilityTransition(to: targetVisible, source: source)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func performDeveloperToolsVisibilityTransition(
|
||||
to targetVisible: Bool,
|
||||
source: String
|
||||
) -> Bool {
|
||||
guard let inspector = webView.cmuxInspectorObject() else { return false }
|
||||
|
||||
let isVisibleSelector = NSSelectorFromString("isVisible")
|
||||
let visible = inspector.cmuxCallBool(selector: isVisibleSelector) ?? false
|
||||
let targetVisible = !visible
|
||||
preferredDeveloperToolsVisible = targetVisible
|
||||
developerToolsTransitionTargetVisible = targetVisible
|
||||
|
||||
if targetVisible {
|
||||
_ = revealDeveloperTools(inspector)
|
||||
if !visible {
|
||||
_ = revealDeveloperTools(inspector)
|
||||
} else {
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
}
|
||||
} else {
|
||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||
guard concealDeveloperTools(inspector) else { return false }
|
||||
if visible {
|
||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||
guard concealDeveloperTools(inspector) else {
|
||||
developerToolsTransitionTargetVisible = nil
|
||||
return false
|
||||
}
|
||||
}
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
}
|
||||
preferredDeveloperToolsVisible = targetVisible
|
||||
|
||||
if targetVisible {
|
||||
let visibleAfterToggle = inspector.cmuxCallBool(selector: isVisibleSelector) ?? false
|
||||
if visibleAfterToggle {
|
||||
let visibleAfterTransition = inspector.cmuxCallBool(selector: isVisibleSelector) ?? false
|
||||
if visibleAfterTransition {
|
||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||
cancelDeveloperToolsRestoreRetry()
|
||||
scheduleDetachedDeveloperToolsWindowDismissal()
|
||||
|
|
@ -3036,6 +3110,26 @@ extension BrowserPanel {
|
|||
cancelDeveloperToolsRestoreRetry()
|
||||
forceDeveloperToolsRefreshOnNextAttach = false
|
||||
}
|
||||
|
||||
if visible != targetVisible {
|
||||
scheduleDeveloperToolsTransitionSettle(source: source)
|
||||
} else {
|
||||
developerToolsTransitionTargetVisible = nil
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func toggleDeveloperTools() -> Bool {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.devtools toggle.begin panel=\(id.uuidString.prefix(5)) " +
|
||||
"\(debugDeveloperToolsStateSummary()) \(debugDeveloperToolsGeometrySummary())"
|
||||
)
|
||||
#endif
|
||||
let targetVisible = !effectiveDeveloperToolsVisibilityIntent()
|
||||
let handled = enqueueDeveloperToolsVisibilityTransition(to: targetVisible, source: "toggle")
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"browser.devtools toggle.end panel=\(id.uuidString.prefix(5)) targetVisible=\(targetVisible ? 1 : 0) " +
|
||||
|
|
@ -3049,30 +3143,18 @@ extension BrowserPanel {
|
|||
)
|
||||
}
|
||||
#endif
|
||||
return true
|
||||
return handled
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showDeveloperTools() -> Bool {
|
||||
guard let inspector = webView.cmuxInspectorObject() else { return false }
|
||||
let visible = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) ?? false
|
||||
if !visible {
|
||||
guard revealDeveloperTools(inspector) else { return false }
|
||||
}
|
||||
preferredDeveloperToolsVisible = true
|
||||
if (inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) ?? false) {
|
||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||
cancelDeveloperToolsRestoreRetry()
|
||||
scheduleDetachedDeveloperToolsWindowDismissal()
|
||||
} else {
|
||||
scheduleDeveloperToolsRestoreRetry()
|
||||
}
|
||||
return true
|
||||
return enqueueDeveloperToolsVisibilityTransition(to: true, source: "show")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func showDeveloperToolsConsole() -> Bool {
|
||||
guard showDeveloperTools() else { return false }
|
||||
guard !isDeveloperToolsTransitionInFlight else { return true }
|
||||
guard let inspector = webView.cmuxInspectorObject() else { return true }
|
||||
// WebKit private inspector API differs by OS; try known console selectors.
|
||||
let consoleSelectors = [
|
||||
|
|
@ -3094,6 +3176,20 @@ extension BrowserPanel {
|
|||
func syncDeveloperToolsPreferenceFromInspector(preserveVisibleIntent: Bool = false) {
|
||||
guard let inspector = webView.cmuxInspectorObject() else { return }
|
||||
guard let visible = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) else { return }
|
||||
if isDeveloperToolsTransitionInFlight {
|
||||
let targetVisible = pendingDeveloperToolsTransitionTargetVisible ?? developerToolsTransitionTargetVisible ?? visible
|
||||
preferredDeveloperToolsVisible = targetVisible
|
||||
if targetVisible, visible {
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||
cancelDeveloperToolsRestoreRetry()
|
||||
} else if !targetVisible {
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
forceDeveloperToolsRefreshOnNextAttach = false
|
||||
cancelDeveloperToolsRestoreRetry()
|
||||
}
|
||||
return
|
||||
}
|
||||
if visible {
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||
|
|
@ -3115,6 +3211,7 @@ extension BrowserPanel {
|
|||
forceDeveloperToolsRefreshOnNextAttach = false
|
||||
return
|
||||
}
|
||||
guard !isDeveloperToolsTransitionInFlight else { return }
|
||||
guard let inspector = webView.cmuxInspectorObject() else {
|
||||
scheduleDeveloperToolsRestoreRetry()
|
||||
return
|
||||
|
|
@ -3180,17 +3277,7 @@ extension BrowserPanel {
|
|||
|
||||
@discardableResult
|
||||
func hideDeveloperTools() -> Bool {
|
||||
guard let inspector = webView.cmuxInspectorObject() else { return false }
|
||||
let visible = inspector.cmuxCallBool(selector: NSSelectorFromString("isVisible")) ?? false
|
||||
if visible {
|
||||
syncDeveloperToolsPresentationPreferenceFromUI()
|
||||
guard concealDeveloperTools(inspector) else { return false }
|
||||
}
|
||||
preferredDeveloperToolsVisible = false
|
||||
developerToolsDetachedOpenGraceDeadline = nil
|
||||
forceDeveloperToolsRefreshOnNextAttach = false
|
||||
cancelDeveloperToolsRestoreRetry()
|
||||
return true
|
||||
return enqueueDeveloperToolsVisibilityTransition(to: false, source: "hide")
|
||||
}
|
||||
|
||||
/// During split/layout transitions SwiftUI can briefly mark the browser surface hidden
|
||||
|
|
@ -4056,7 +4143,9 @@ extension BrowserPanel {
|
|||
let attached = webView.superview == nil ? 0 : 1
|
||||
let inWindow = webView.window == nil ? 0 : 1
|
||||
let forceRefresh = forceDeveloperToolsRefreshOnNextAttach ? 1 : 0
|
||||
return "pref=\(preferred) vis=\(visible) inspector=\(inspector) attached=\(attached) inWindow=\(inWindow) restoreRetry=\(developerToolsRestoreRetryAttempt) forceRefresh=\(forceRefresh)"
|
||||
let transitionTarget = developerToolsTransitionTargetVisible.map { $0 ? "1" : "0" } ?? "nil"
|
||||
let pendingTarget = pendingDeveloperToolsTransitionTargetVisible.map { $0 ? "1" : "0" } ?? "nil"
|
||||
return "pref=\(preferred) vis=\(visible) inspector=\(inspector) attached=\(attached) inWindow=\(inWindow) restoreRetry=\(developerToolsRestoreRetryAttempt) forceRefresh=\(forceRefresh) tx=\(transitionTarget) pending=\(pendingTarget)"
|
||||
}
|
||||
|
||||
func debugDeveloperToolsGeometrySummary() -> String {
|
||||
|
|
|
|||
|
|
@ -3716,6 +3716,12 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
final class HostContainerView: NSView {
|
||||
private final class HostedInspectorSideDockContainerView: NSView {
|
||||
override var isOpaque: Bool { false }
|
||||
|
||||
override func resizeSubviews(withOldSize oldSize: NSSize) {
|
||||
// Managed side-docked DevTools use explicit frame updates from the host.
|
||||
// Letting AppKit autoresize the WK siblings here makes them snap back to
|
||||
// stale widths while the divider drag or pane resize is in flight.
|
||||
}
|
||||
}
|
||||
|
||||
var onDidMoveToWindow: (() -> Void)?
|
||||
|
|
@ -3760,7 +3766,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
}
|
||||
|
||||
private static let hostedInspectorDividerHitExpansion: CGFloat = 10
|
||||
private static let minimumHostedInspectorWidth: CGFloat = 1
|
||||
private static let minimumHostedInspectorWidth: CGFloat = 120
|
||||
private var trackingArea: NSTrackingArea?
|
||||
private var activeDividerCursorKind: DividerCursorKind?
|
||||
private var hostedInspectorDividerDrag: HostedInspectorDividerDragState?
|
||||
|
|
@ -4116,6 +4122,21 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
layoutHostedInspectorSideDockIfNeeded(reason: "sideDock.activate")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded() -> Bool {
|
||||
guard !isHostedInspectorSideDockActive(),
|
||||
let slotView = localInlineSlotView,
|
||||
let hit = hostedInspectorDividerCandidateUsingKnownWebViews(in: slotView) else {
|
||||
return false
|
||||
}
|
||||
|
||||
// The inspector frontend sometimes reports its dock configuration a tick
|
||||
// late after local-inline reattach. Promote the visible left/right split
|
||||
// immediately so drag routing stays symmetric on both dock sides.
|
||||
activateHostedInspectorSideDockIfNeeded(using: hit)
|
||||
return isHostedInspectorSideDockActive()
|
||||
}
|
||||
|
||||
private func deactivateHostedInspectorSideDockIfNeeded(reparentTo slotView: WindowBrowserSlotView?) {
|
||||
guard let slotView,
|
||||
let pageView = hostedInspectorSideDockPageView,
|
||||
|
|
@ -4151,6 +4172,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
inspectorView: inspectorView,
|
||||
dockSide: dockSide
|
||||
),
|
||||
minimumInspectorWidth: Self.minimumHostedInspectorWidth,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
|
|
@ -4236,11 +4258,15 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
_ = promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded()
|
||||
if let previousSize = lastHostedInspectorLayoutBoundsSize,
|
||||
Self.sizeApproximatelyEqual(previousSize, bounds.size, epsilon: 0.5) {
|
||||
if isHostedInspectorSideDockActive() {
|
||||
layoutHostedInspectorSideDockIfNeeded(reason: "host.layout.sideDock.sameSize")
|
||||
} else if !isHostedInspectorDividerDragActive && !hasStoredHostedInspectorWidthPreference {
|
||||
// Origin-only frame churn is common while the surrounding split layout
|
||||
// settles. Reapplying the side-docked inspector at the same size fights
|
||||
// WebKit's own dock layout and shows up as visible flicker.
|
||||
if !isHostedInspectorSideDockActive() &&
|
||||
!isHostedInspectorDividerDragActive &&
|
||||
!hasStoredHostedInspectorWidthPreference {
|
||||
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout.sameSize")
|
||||
}
|
||||
notifyGeometryChangedIfNeeded()
|
||||
|
|
@ -4264,9 +4290,6 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
|
||||
override func setFrameOrigin(_ newOrigin: NSPoint) {
|
||||
super.setFrameOrigin(newOrigin)
|
||||
if isHostedInspectorSideDockActive() {
|
||||
layoutHostedInspectorSideDockIfNeeded(reason: "setFrameOrigin.sideDock")
|
||||
}
|
||||
window?.invalidateCursorRects(for: self)
|
||||
notifyGeometryChangedIfNeeded()
|
||||
#if DEBUG
|
||||
|
|
@ -4276,9 +4299,6 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
|
||||
override func setFrameSize(_ newSize: NSSize) {
|
||||
super.setFrameSize(newSize)
|
||||
if isHostedInspectorSideDockActive() {
|
||||
layoutHostedInspectorSideDockIfNeeded(reason: "setFrameSize.sideDock")
|
||||
}
|
||||
window?.invalidateCursorRects(for: self)
|
||||
notifyGeometryChangedIfNeeded()
|
||||
#if DEBUG
|
||||
|
|
@ -4419,6 +4439,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
inspectorView: dragState.inspectorView,
|
||||
dockSide: dragState.dockSide
|
||||
),
|
||||
minimumInspectorWidth: Self.minimumHostedInspectorWidth,
|
||||
reason: "drag"
|
||||
)
|
||||
#if DEBUG
|
||||
|
|
@ -4698,6 +4719,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let self else { return }
|
||||
self.hostedInspectorReapplyWorkItem = nil
|
||||
_ = self.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded()
|
||||
if self.isHostedInspectorSideDockActive() {
|
||||
self.reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: reason)
|
||||
} else if !self.hasStoredHostedInspectorWidthPreference {
|
||||
|
|
@ -4766,13 +4788,19 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
}
|
||||
let currentInspectorWidth = max(0, hit.inspectorView.frame.width)
|
||||
guard abs(currentInspectorWidth - preferredWidth) > 0.5 else { return }
|
||||
_ = applyHostedInspectorDividerWidth(preferredWidth, to: hit, reason: reason)
|
||||
_ = applyHostedInspectorDividerWidth(
|
||||
preferredWidth,
|
||||
to: hit,
|
||||
minimumInspectorWidth: Self.minimumHostedInspectorWidth,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func applyHostedInspectorDividerWidth(
|
||||
_ preferredWidth: CGFloat,
|
||||
to hit: HostedInspectorDividerHit,
|
||||
minimumInspectorWidth: CGFloat,
|
||||
reason: String
|
||||
) -> (pageFrame: NSRect, inspectorFrame: NSRect) {
|
||||
let containerBounds = hit.containerView.bounds
|
||||
|
|
@ -4781,7 +4809,7 @@ struct WebViewRepresentable: NSViewRepresentable {
|
|||
in: containerBounds,
|
||||
pageFrame: hit.pageView.frame,
|
||||
inspectorFrame: hit.inspectorView.frame,
|
||||
minimumInspectorWidth: 0
|
||||
minimumInspectorWidth: minimumInspectorWidth
|
||||
)
|
||||
let pageFrame = nextFrames.pageFrame
|
||||
let inspectorFrame = nextFrames.inspectorFrame
|
||||
|
|
|
|||
|
|
@ -2493,6 +2493,10 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
return nil
|
||||
}
|
||||
|
||||
private func waitForDeveloperToolsTransitions() {
|
||||
RunLoop.current.run(until: Date().addingTimeInterval(0.5))
|
||||
}
|
||||
|
||||
func testRestoreReopensInspectorAfterAttachWhenPreferredVisible() {
|
||||
let (panel, inspector) = makePanelWithInspector()
|
||||
|
||||
|
|
@ -2574,6 +2578,37 @@ final class BrowserDeveloperToolsVisibilityPersistenceTests: XCTestCase {
|
|||
XCTAssertFalse(panel.hasPendingDeveloperToolsRefreshAfterAttach())
|
||||
}
|
||||
|
||||
func testRapidToggleCoalescesToFinalVisibleIntentWithoutExtraInspectorCalls() {
|
||||
let (panel, inspector) = makePanelWithInspector()
|
||||
|
||||
XCTAssertTrue(panel.toggleDeveloperTools())
|
||||
XCTAssertTrue(panel.toggleDeveloperTools())
|
||||
XCTAssertTrue(panel.toggleDeveloperTools())
|
||||
XCTAssertEqual(inspector.showCount, 1)
|
||||
XCTAssertEqual(inspector.closeCount, 0)
|
||||
|
||||
waitForDeveloperToolsTransitions()
|
||||
|
||||
XCTAssertTrue(panel.isDeveloperToolsVisible())
|
||||
XCTAssertEqual(inspector.showCount, 1)
|
||||
XCTAssertEqual(inspector.closeCount, 0)
|
||||
}
|
||||
|
||||
func testRapidToggleQueuesHideAfterOpenTransitionSettles() {
|
||||
let (panel, inspector) = makePanelWithInspector()
|
||||
|
||||
XCTAssertTrue(panel.toggleDeveloperTools())
|
||||
XCTAssertTrue(panel.toggleDeveloperTools())
|
||||
XCTAssertEqual(inspector.showCount, 1)
|
||||
XCTAssertEqual(inspector.closeCount, 0)
|
||||
|
||||
waitForDeveloperToolsTransitions()
|
||||
|
||||
XCTAssertFalse(panel.isDeveloperToolsVisible())
|
||||
XCTAssertEqual(inspector.showCount, 1)
|
||||
XCTAssertEqual(inspector.closeCount, 1)
|
||||
}
|
||||
|
||||
func testTransientHideAttachmentPreserveFollowsDeveloperToolsIntent() {
|
||||
let (panel, _) = makePanelWithInspector()
|
||||
|
||||
|
|
@ -9282,6 +9317,45 @@ final class BrowserPanelHostContainerViewTests: XCTestCase {
|
|||
XCTAssertGreaterThan(inspectorContainer.frame.minX, 0)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostClaimsHostedInspectorDividerAcrossFullHeight() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 240, height: contentView.bounds.height))
|
||||
host.autoresizingMask = [.minXMargin, .height]
|
||||
contentView.addSubview(host)
|
||||
|
||||
let webViewRoot = NSView(frame: host.bounds)
|
||||
webViewRoot.autoresizingMask = [.width, .height]
|
||||
host.addSubview(webViewRoot)
|
||||
|
||||
let pageView = PrimaryPageProbeView(frame: NSRect(x: 0, y: 20, width: 92, height: webViewRoot.bounds.height - 40))
|
||||
let inspectorContainer = EdgeTransparentWKInspectorProbeView(
|
||||
frame: NSRect(x: 92, y: 20, width: webViewRoot.bounds.width - 92, height: webViewRoot.bounds.height - 40)
|
||||
)
|
||||
webViewRoot.addSubview(pageView)
|
||||
webViewRoot.addSubview(inspectorContainer)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertTrue(
|
||||
host.hitTest(NSPoint(x: inspectorContainer.frame.minX + 2, y: 4)) === host,
|
||||
"The custom DevTools divider should remain draggable at the top edge of the browser pane"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
host.hitTest(NSPoint(x: inspectorContainer.frame.minX + 2, y: host.bounds.maxY - 4)) === host,
|
||||
"The custom DevTools divider should remain draggable at the bottom edge of the browser pane"
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostFallsBackToManualHostedInspectorDragWhenNativeDividerHitIsUnavailable() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
|
|
@ -9333,6 +9407,301 @@ final class BrowserPanelHostContainerViewTests: XCTestCase {
|
|||
XCTAssertGreaterThan(inspectorContainer.frame.minX, 92)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostKeepsInspectorResizableAfterShrinkingToMinimumWidth() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 240, height: contentView.bounds.height))
|
||||
host.autoresizingMask = [.minXMargin, .height]
|
||||
contentView.addSubview(host)
|
||||
|
||||
let webViewRoot = NSView(frame: host.bounds)
|
||||
webViewRoot.autoresizingMask = [.width, .height]
|
||||
host.addSubview(webViewRoot)
|
||||
|
||||
let pageView = PrimaryPageProbeView(frame: NSRect(x: 0, y: 0, width: 92, height: webViewRoot.bounds.height))
|
||||
let inspectorContainer = EdgeTransparentWKInspectorProbeView(
|
||||
frame: NSRect(x: 92, y: 0, width: webViewRoot.bounds.width - 92, height: webViewRoot.bounds.height)
|
||||
)
|
||||
webViewRoot.addSubview(pageView)
|
||||
webViewRoot.addSubview(inspectorContainer)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
|
||||
let dividerPointInHost = NSPoint(x: inspectorContainer.frame.minX + 2, y: host.bounds.midY)
|
||||
let dividerPointInWindow = host.convert(dividerPointInHost, to: nil)
|
||||
|
||||
host.mouseDown(with: makeMouseEvent(type: .leftMouseDown, location: dividerPointInWindow, window: window))
|
||||
let drag = makeMouseEvent(
|
||||
type: .leftMouseDragged,
|
||||
location: NSPoint(x: dividerPointInWindow.x + 220, y: dividerPointInWindow.y),
|
||||
window: window
|
||||
)
|
||||
host.mouseDragged(with: drag)
|
||||
host.mouseUp(with: makeMouseEvent(type: .leftMouseUp, location: drag.locationInWindow, window: window))
|
||||
|
||||
XCTAssertGreaterThanOrEqual(
|
||||
inspectorContainer.frame.width,
|
||||
120,
|
||||
"Shrinking the DevTools pane should clamp to a recoverable minimum width"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
host.hitTest(NSPoint(x: inspectorContainer.frame.minX + 2, y: 4)) === host,
|
||||
"After clamping, the DevTools divider should still be draggable near the top edge"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
host.hitTest(NSPoint(x: inspectorContainer.frame.minX + 2, y: host.bounds.maxY - 4)) === host,
|
||||
"After clamping, the DevTools divider should still be draggable near the bottom edge"
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostPromotesVisibleRightDockedInspectorIntoManagedSideDock() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 240, height: contentView.bounds.height))
|
||||
host.autoresizingMask = [.minXMargin, .height]
|
||||
contentView.addSubview(host)
|
||||
|
||||
let slotView = host.ensureLocalInlineSlotView()
|
||||
let pageView = WKWebView(frame: NSRect(x: 0, y: 0, width: 92, height: host.bounds.height + 180))
|
||||
let inspectorView = WKWebView(
|
||||
frame: NSRect(x: 92, y: 0, width: slotView.bounds.width - 92, height: host.bounds.height)
|
||||
)
|
||||
slotView.addSubview(pageView)
|
||||
slotView.addSubview(inspectorView)
|
||||
host.pinHostedWebView(pageView, in: slotView)
|
||||
host.setHostedInspectorFrontendWebView(inspectorView)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
host.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertTrue(
|
||||
host.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded(),
|
||||
"A visible right-docked inspector should not wait on async dock-configuration JS before entering the managed side-dock path"
|
||||
)
|
||||
XCTAssertTrue(
|
||||
pageView.superview === inspectorView.superview && pageView.superview !== slotView,
|
||||
"Promotion should move both hosted inspector siblings into the managed side-dock container"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
pageView.frame.height,
|
||||
host.bounds.height,
|
||||
accuracy: 0.5,
|
||||
"Promotion should normalize stale page heights to the host height so the page layer stops covering the divider"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inspectorView.frame.height,
|
||||
host.bounds.height,
|
||||
accuracy: 0.5,
|
||||
"Promotion should normalize the inspector height to the host height"
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostAllowsRightDockedInspectorToExpandLeftAfterPromotion() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 240, height: contentView.bounds.height))
|
||||
host.autoresizingMask = [.minXMargin, .height]
|
||||
contentView.addSubview(host)
|
||||
|
||||
let slotView = host.ensureLocalInlineSlotView()
|
||||
let pageView = WKWebView(frame: NSRect(x: 0, y: 0, width: 92, height: host.bounds.height))
|
||||
let inspectorView = WKWebView(
|
||||
frame: NSRect(x: 92, y: 0, width: slotView.bounds.width - 92, height: host.bounds.height)
|
||||
)
|
||||
slotView.addSubview(pageView)
|
||||
slotView.addSubview(inspectorView)
|
||||
host.pinHostedWebView(pageView, in: slotView)
|
||||
host.setHostedInspectorFrontendWebView(inspectorView)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
host.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertTrue(
|
||||
host.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded(),
|
||||
"The managed side-dock path should be active before drag assertions run"
|
||||
)
|
||||
|
||||
let initialPageWidth = pageView.frame.width
|
||||
let initialInspectorWidth = inspectorView.frame.width
|
||||
let dividerPointInHost = NSPoint(x: inspectorView.frame.minX + 2, y: host.bounds.midY)
|
||||
let dividerPointInWindow = host.convert(dividerPointInHost, to: nil)
|
||||
|
||||
host.mouseDown(with: makeMouseEvent(type: .leftMouseDown, location: dividerPointInWindow, window: window))
|
||||
let drag = makeMouseEvent(
|
||||
type: .leftMouseDragged,
|
||||
location: NSPoint(x: dividerPointInWindow.x - 40, y: dividerPointInWindow.y),
|
||||
window: window
|
||||
)
|
||||
host.mouseDragged(with: drag)
|
||||
host.mouseUp(with: makeMouseEvent(type: .leftMouseUp, location: drag.locationInWindow, window: window))
|
||||
|
||||
XCTAssertGreaterThan(
|
||||
inspectorView.frame.width,
|
||||
initialInspectorWidth,
|
||||
"Right-docked DevTools should expand when the divider is dragged left"
|
||||
)
|
||||
XCTAssertLessThan(
|
||||
pageView.frame.width,
|
||||
initialPageWidth,
|
||||
"Expanding right-docked DevTools should shrink the page width"
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostKeepsAutomaticRightDockedWidthAboveMinimumWhileShrinking() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 140, y: 0, width: 280, height: contentView.bounds.height))
|
||||
host.autoresizingMask = [.minXMargin, .height]
|
||||
contentView.addSubview(host)
|
||||
|
||||
let slotView = host.ensureLocalInlineSlotView()
|
||||
let pageView = WKWebView(frame: NSRect(x: 0, y: 0, width: 132, height: host.bounds.height))
|
||||
let inspectorView = WKWebView(
|
||||
frame: NSRect(x: 132, y: 0, width: slotView.bounds.width - 132, height: host.bounds.height)
|
||||
)
|
||||
slotView.addSubview(pageView)
|
||||
slotView.addSubview(inspectorView)
|
||||
host.pinHostedWebView(pageView, in: slotView)
|
||||
host.setHostedInspectorFrontendWebView(inspectorView)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
host.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertTrue(host.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded())
|
||||
|
||||
host.setPreferredHostedInspectorWidth(width: 80, widthFraction: nil)
|
||||
host.setFrameSize(NSSize(width: 210, height: host.frame.height))
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
host.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertGreaterThanOrEqual(
|
||||
inspectorView.frame.width,
|
||||
120,
|
||||
"Automatic pane resize should honor the same minimum hosted inspector width as manual dragging"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inspectorView.frame.height,
|
||||
host.bounds.height,
|
||||
accuracy: 0.5,
|
||||
"Automatic shrink should keep the inspector vertically normalized to the host height"
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserPanelManagedSideDockDoesNotAutoresizeDraggedFrames() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
defer { window.orderOut(nil) }
|
||||
guard let contentView = window.contentView else {
|
||||
XCTFail("Expected content view")
|
||||
return
|
||||
}
|
||||
|
||||
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 240, height: contentView.bounds.height))
|
||||
host.autoresizingMask = [.minXMargin, .height]
|
||||
contentView.addSubview(host)
|
||||
|
||||
let slotView = host.ensureLocalInlineSlotView()
|
||||
let pageView = WKWebView(frame: NSRect(x: 0, y: 0, width: 92, height: host.bounds.height))
|
||||
let inspectorView = WKWebView(
|
||||
frame: NSRect(x: 92, y: 0, width: slotView.bounds.width - 92, height: host.bounds.height)
|
||||
)
|
||||
slotView.addSubview(pageView)
|
||||
slotView.addSubview(inspectorView)
|
||||
host.pinHostedWebView(pageView, in: slotView)
|
||||
host.setHostedInspectorFrontendWebView(inspectorView)
|
||||
contentView.layoutSubtreeIfNeeded()
|
||||
host.layoutSubtreeIfNeeded()
|
||||
|
||||
XCTAssertTrue(host.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded())
|
||||
|
||||
let dividerPointInHost = NSPoint(x: inspectorView.frame.minX + 2, y: host.bounds.midY)
|
||||
let dividerPointInWindow = host.convert(dividerPointInHost, to: nil)
|
||||
host.mouseDown(with: makeMouseEvent(type: .leftMouseDown, location: dividerPointInWindow, window: window))
|
||||
let drag = makeMouseEvent(
|
||||
type: .leftMouseDragged,
|
||||
location: NSPoint(x: dividerPointInWindow.x - 30, y: dividerPointInWindow.y),
|
||||
window: window
|
||||
)
|
||||
host.mouseDragged(with: drag)
|
||||
host.mouseUp(with: makeMouseEvent(type: .leftMouseUp, location: drag.locationInWindow, window: window))
|
||||
|
||||
guard let managedContainer = pageView.superview else {
|
||||
XCTFail("Expected managed side-dock container")
|
||||
return
|
||||
}
|
||||
let draggedPageFrame = pageView.frame
|
||||
let draggedInspectorFrame = inspectorView.frame
|
||||
|
||||
managedContainer.setFrameSize(
|
||||
NSSize(width: managedContainer.frame.width, height: managedContainer.frame.height + 24)
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
pageView.frame.origin.x,
|
||||
draggedPageFrame.origin.x,
|
||||
accuracy: 0.5,
|
||||
"Managed side-dock container should not autoresize the page back to a stale divider position"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
pageView.frame.width,
|
||||
draggedPageFrame.width,
|
||||
accuracy: 0.5,
|
||||
"Managed side-dock container should preserve the dragged page width until the host explicitly reapplies layout"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inspectorView.frame.origin.x,
|
||||
draggedInspectorFrame.origin.x,
|
||||
accuracy: 0.5,
|
||||
"Managed side-dock container should preserve the dragged inspector origin"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
inspectorView.frame.width,
|
||||
draggedInspectorFrame.width,
|
||||
accuracy: 0.5,
|
||||
"Managed side-dock container should preserve the dragged inspector width"
|
||||
)
|
||||
}
|
||||
|
||||
func testBrowserPanelHostFallsBackToManualHostedInspectorDragForLeftDockedInspector() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
|
||||
|
|
@ -11445,6 +11814,33 @@ final class BrowserWindowPortalLifecycleTests: XCTestCase {
|
|||
XCTAssertEqual(webView.frame.size.height, slot.bounds.size.height, accuracy: 0.5)
|
||||
}
|
||||
|
||||
func testPortalSlotPinPreservesSideDockedInspectorManagedWebViewFrameOnRehost() {
|
||||
let slot = WindowBrowserSlotView(frame: NSRect(x: 0, y: 0, width: 240, height: 160))
|
||||
let webView = CmuxWebView(frame: NSRect(x: 0, y: 0, width: 132, height: 160), configuration: WKWebViewConfiguration())
|
||||
let inspectorContainer = NSView(frame: NSRect(x: 132, y: 0, width: 108, height: 160))
|
||||
let inspectorView = WKInspectorProbeView(frame: inspectorContainer.bounds)
|
||||
inspectorView.autoresizingMask = [.width, .height]
|
||||
inspectorContainer.addSubview(inspectorView)
|
||||
slot.addSubview(webView)
|
||||
slot.addSubview(inspectorContainer)
|
||||
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
webView.autoresizingMask = []
|
||||
slot.pinHostedWebView(webView)
|
||||
|
||||
XCTAssertEqual(
|
||||
webView.frame.maxX,
|
||||
inspectorContainer.frame.minX,
|
||||
accuracy: 0.5,
|
||||
"Rehosting a portal-managed browser should preserve the WebKit-owned side inspector split"
|
||||
)
|
||||
XCTAssertLessThan(
|
||||
webView.frame.width,
|
||||
slot.bounds.width,
|
||||
"The page frame should stay narrower than the full slot while a side-docked inspector is present"
|
||||
)
|
||||
}
|
||||
|
||||
func testPortalResizePreservesSideDockedInspectorManagedWebViewFrame() {
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 520, height: 320),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue