Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 157 additions & 177 deletions Sources/BrowserWindowPortal.swift

Large diffs are not rendered by default.

399 changes: 183 additions & 216 deletions Sources/Panels/BrowserPanelView.swift

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions Sources/Panels/HostedInspectorAttachedSizeSync.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import AppKit
import WebKit

@MainActor
struct HostedInspectorAttachedSizeSync {
static func sync(frontendWebView: WKWebView?, dockSide: HostedInspectorDockSide, extent: CGFloat) {
Comment on lines +5 to +6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Static Inspector Sync Namespace

This new production type is only a static helper surface, so the WebKit size-sync behavior has no owning host instance to construct or inject. The repo rule asks production Swift to keep behavior on the scoped owner; here the operation is always called from the inspector-hosting views after a drag, so it should live behind that owner instead of a new ambient namespace.

Rule Used: Flag new ambient global state in production Swift:... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional: the sync is invoked from two independent hosts (window-portal WindowBrowserHostView and local-inline HostContainerView) after their drag loops, and per the repo's shared-behavior policy that must be one shared mutation path, not per-host copies. It is stateless (pure JS construction + fire-and-forget evaluate), takes the frontend webview explicitly rather than reaching into ambient state, and follows the existing pattern of BrowserDeveloperToolsDockControlNormalizer/HostedInspectorDockControlScript. Happy to fold it into a host-owned instance if a future refactor gives the two hosts a common owner.

guard let frontendWebView else { return }
frontendWebView.evaluateJavaScript(
javaScript(dockSide: dockSide, extent: extent),
completionHandler: nil
)
}

static func sync(pageWebView: WKWebView?, dockSide: HostedInspectorDockSide, extent: CGFloat) {
sync(
frontendWebView: pageWebView?.cmuxInspectorFrontendWebView(),
dockSide: dockSide,
extent: extent
)
}

nonisolated static func javaScript(dockSide: HostedInspectorDockSide, extent: CGFloat) -> String {
let method = dockSide.isHorizontalDivider ? "setAttachedWindowHeight" : "setAttachedWindowWidth"
let roundedExtent = max(0, Int(extent.rounded()))
return """
(() => {
if (typeof InspectorFrontendHost === "undefined") { return false; }
if (typeof InspectorFrontendHost.\(method) !== "function") { return false; }
InspectorFrontendHost.\(method)(\(roundedExtent));
return true;
})();
"""
}
}
260 changes: 260 additions & 0 deletions Sources/Panels/HostedInspectorResizeGeometry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import AppKit

struct HostedInspectorMinimumSizePolicy: Equatable {
let minimumInspectorExtent: CGFloat
let minimumPageExtent: CGFloat

static let sideDock = HostedInspectorMinimumSizePolicy(
minimumInspectorExtent: 120,
minimumPageExtent: 120
)

static let bottomDock = HostedInspectorMinimumSizePolicy(
minimumInspectorExtent: 100,
minimumPageExtent: 80
)

init(minimumInspectorExtent: CGFloat, minimumPageExtent: CGFloat) {
self.minimumInspectorExtent = max(0, minimumInspectorExtent)
self.minimumPageExtent = max(0, minimumPageExtent)
}

init(dockSide: HostedInspectorDockSide) {
self = dockSide == .bottom ? .bottomDock : .sideDock
}

func clampedInspectorExtent(_ proposedExtent: CGFloat, containerExtent: CGFloat) -> CGFloat {
let extent = max(0, containerExtent)
let effectiveMinInspector = min(minimumInspectorExtent, extent / 2)
let effectiveMinPage = min(minimumPageExtent, extent / 2)
let maxInspector = max(effectiveMinInspector, extent - effectiveMinPage)
return min(maxInspector, max(effectiveMinInspector, proposedExtent))
}
}

enum HostedInspectorDockSide {
case leading
case trailing
case bottom

static func resolve(
pageFrame: NSRect,
inspectorFrame: NSRect,
epsilon: CGFloat = 4
) -> Self? {
let verticalOverlap = verticalOverlap(between: pageFrame, and: inspectorFrame)
let horizontalOverlap = horizontalOverlap(between: pageFrame, and: inspectorFrame)
if verticalOverlap > 0, pageFrame.maxX <= inspectorFrame.minX + epsilon {
return .trailing
}
if verticalOverlap > 0, inspectorFrame.maxX <= pageFrame.minX + epsilon {
return .leading
}
if horizontalOverlap > 0, inspectorFrame.maxY <= pageFrame.minY + epsilon {
return .bottom
}
return nil
}

var isHorizontalDivider: Bool {
self == .bottom
}

func dividerPosition(pageFrame: NSRect, inspectorFrame: NSRect) -> CGFloat {
switch self {
case .leading:
return inspectorFrame.maxX
case .trailing:
return inspectorFrame.minX
case .bottom:
return inspectorFrame.maxY
}
}

func dividerHitRect(
in bounds: NSRect,
pageFrame: NSRect,
inspectorFrame: NSRect,
expansion: CGFloat
) -> NSRect {
let position = dividerPosition(pageFrame: pageFrame, inspectorFrame: inspectorFrame)
switch self {
case .leading, .trailing:
return NSRect(
x: position - expansion,
y: bounds.minY,
width: expansion * 2,
height: max(0, bounds.height)
)
case .bottom:
return NSRect(
x: bounds.minX,
y: position - expansion,
width: max(0, bounds.width),
height: expansion * 2
)
}
}

func clampedDividerPosition(
_ proposedDividerPosition: CGFloat,
containerBounds: NSRect,
pageFrame: NSRect,
inspectorFrame: NSRect,
policy: HostedInspectorMinimumSizePolicy
) -> CGFloat {
let proposedExtent = inspectorExtent(
forDividerPosition: proposedDividerPosition,
in: containerBounds
)
let clampedExtent = policy.clampedInspectorExtent(
proposedExtent,
containerExtent: containerExtent(in: containerBounds)
)
return dividerPosition(forInspectorExtent: clampedExtent, in: containerBounds)
}

func inspectorExtent(forDividerPosition dividerPosition: CGFloat, in containerBounds: NSRect) -> CGFloat {
switch self {
case .leading:
return max(0, dividerPosition - containerBounds.minX)
case .trailing:
return max(0, containerBounds.maxX - dividerPosition)
case .bottom:
return max(0, dividerPosition - containerBounds.minY)
}
}

func inspectorExtent(inspectorFrame: NSRect, in containerBounds: NSRect) -> CGFloat {
switch self {
case .leading, .trailing:
return min(max(0, containerBounds.width), max(0, inspectorFrame.width))
case .bottom:
return min(max(0, containerBounds.height), max(0, inspectorFrame.height))
}
}

func resizedFrames(
preferredExtent: CGFloat,
in containerBounds: NSRect,
pageFrame: NSRect,
inspectorFrame: NSRect,
policy: HostedInspectorMinimumSizePolicy
) -> (pageFrame: NSRect, inspectorFrame: NSRect) {
let clampedExtent = policy.clampedInspectorExtent(
preferredExtent,
containerExtent: containerExtent(in: containerBounds)
)
let dividerPosition = dividerPosition(forInspectorExtent: clampedExtent, in: containerBounds)
switch self {
case .leading:
return horizontalFrames(
dividerX: dividerPosition,
containerBounds: containerBounds,
pageFrame: pageFrame,
inspectorFrame: inspectorFrame,
inspectorOnLeadingEdge: true
)
case .trailing:
return horizontalFrames(
dividerX: dividerPosition,
containerBounds: containerBounds,
pageFrame: pageFrame,
inspectorFrame: inspectorFrame,
inspectorOnLeadingEdge: false
)
case .bottom:
var nextPageFrame = pageFrame
nextPageFrame.origin.x = containerBounds.minX
nextPageFrame.origin.y = dividerPosition
nextPageFrame.size.width = max(0, containerBounds.width)
nextPageFrame.size.height = max(0, containerBounds.maxY - dividerPosition)

var nextInspectorFrame = inspectorFrame
nextInspectorFrame.origin.x = containerBounds.minX
nextInspectorFrame.origin.y = containerBounds.minY
nextInspectorFrame.size.width = max(0, containerBounds.width)
nextInspectorFrame.size.height = max(0, dividerPosition - containerBounds.minY)
return (pageFrame: nextPageFrame, inspectorFrame: nextInspectorFrame)
}
}

func hasSufficientCrossAxisOverlap(
pageFrame: NSRect,
inspectorFrame: NSRect,
containerBounds: NSRect
) -> Bool {
let overlap = crossAxisOverlap(pageFrame: pageFrame, inspectorFrame: inspectorFrame)
let containerCrossExtent = isHorizontalDivider ? containerBounds.width : containerBounds.height
return overlap > min(8, max(0, containerCrossExtent) * 0.25)
}

func crossAxisOverlap(pageFrame: NSRect, inspectorFrame: NSRect) -> CGFloat {
switch self {
case .leading, .trailing:
return Self.verticalOverlap(between: pageFrame, and: inspectorFrame)
case .bottom:
return Self.horizontalOverlap(between: pageFrame, and: inspectorFrame)
}
}

func pageExtent(pageFrame: NSRect) -> CGFloat {
switch self {
case .leading, .trailing:
return max(0, pageFrame.width)
case .bottom:
return max(0, pageFrame.height)
}
}

private func containerExtent(in bounds: NSRect) -> CGFloat {
isHorizontalDivider ? bounds.height : bounds.width
}

private func dividerPosition(forInspectorExtent inspectorExtent: CGFloat, in bounds: NSRect) -> CGFloat {
switch self {
case .leading:
return bounds.minX + inspectorExtent
case .trailing:
return bounds.maxX - inspectorExtent
case .bottom:
return bounds.minY + inspectorExtent
}
}

private func horizontalFrames(
dividerX: CGFloat,
containerBounds: NSRect,
pageFrame: NSRect,
inspectorFrame: NSRect,
inspectorOnLeadingEdge: Bool
) -> (pageFrame: NSRect, inspectorFrame: NSRect) {
var nextPageFrame = pageFrame
var nextInspectorFrame = inspectorFrame
nextPageFrame.origin.y = containerBounds.minY
nextPageFrame.size.height = max(0, containerBounds.height)
nextInspectorFrame.origin.y = containerBounds.minY
nextInspectorFrame.size.height = max(0, containerBounds.height)

if inspectorOnLeadingEdge {
nextInspectorFrame.origin.x = containerBounds.minX
nextInspectorFrame.size.width = max(0, dividerX - containerBounds.minX)
nextPageFrame.origin.x = dividerX
nextPageFrame.size.width = max(0, containerBounds.maxX - dividerX)
} else {
nextPageFrame.origin.x = containerBounds.minX
nextPageFrame.size.width = max(0, dividerX - containerBounds.minX)
nextInspectorFrame.origin.x = dividerX
nextInspectorFrame.size.width = max(0, containerBounds.maxX - dividerX)
}
return (pageFrame: nextPageFrame, inspectorFrame: nextInspectorFrame)
}

private static func verticalOverlap(between lhs: NSRect, and rhs: NSRect) -> CGFloat {
max(0, min(lhs.maxY, rhs.maxY) - max(lhs.minY, rhs.minY))
}

private static func horizontalOverlap(between lhs: NSRect, and rhs: NSRect) -> CGFloat {
max(0, min(lhs.maxX, rhs.maxX) - max(lhs.minX, rhs.minX))
}
}
16 changes: 16 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -749,8 +749,12 @@
F5320000A1B2C3D4E5F60718 /* HermesAgentIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5320001A1B2C3D4E5F60718 /* HermesAgentIndex.swift */; };
4931A11B0000000000000002 /* HiddenRightSidebarContentMountingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4931A11B0000000000000001 /* HiddenRightSidebarContentMountingTests.swift */; };
CD0CFE6000000000CD0CFE60 /* HostAccountFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0CFE6100000000CD0CFE61 /* HostAccountFlow.swift */; };
B77620000000000000000004 /* HostedInspectorAttachedSizeSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77620000000000000000003 /* HostedInspectorAttachedSizeSync.swift */; };
B77620000000000000000006 /* HostedInspectorBottomDockDividerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77620000000000000000005 /* HostedInspectorBottomDockDividerTests.swift */; };
B1F0C0030000000000000001 /* HostedInspectorDockControlScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F0C0030000000000000002 /* HostedInspectorDockControlScript.swift */; };
B1F0C0040000000000000001 /* HostedInspectorDockControlScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F0C0040000000000000002 /* HostedInspectorDockControlScriptTests.swift */; };
B77620000000000000000002 /* HostedInspectorResizeGeometry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77620000000000000000001 /* HostedInspectorResizeGeometry.swift */; };
B77620000000000000000008 /* HostedInspectorResizeGeometryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B77620000000000000000007 /* HostedInspectorResizeGeometryTests.swift */; };
CD0CFE6200000000CD0CFE62 /* HostSettingsActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD0CFE6300000000CD0CFE63 /* HostSettingsActions.swift */; };
F1C1AA21B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C1AA20B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift */; };
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
Expand Down Expand Up @@ -2384,8 +2388,12 @@
F5320001A1B2C3D4E5F60718 /* HermesAgentIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HermesAgentIndex.swift; sourceTree = "<group>"; };
4931A11B0000000000000001 /* HiddenRightSidebarContentMountingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenRightSidebarContentMountingTests.swift; sourceTree = "<group>"; };
CD0CFE6100000000CD0CFE61 /* HostAccountFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HostAccountFlow.swift; sourceTree = "<group>"; };
B77620000000000000000003 /* HostedInspectorAttachedSizeSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/HostedInspectorAttachedSizeSync.swift; sourceTree = "<group>"; };
B77620000000000000000005 /* HostedInspectorBottomDockDividerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostedInspectorBottomDockDividerTests.swift; sourceTree = "<group>"; };
B1F0C0030000000000000002 /* HostedInspectorDockControlScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/HostedInspectorDockControlScript.swift; sourceTree = "<group>"; };
B1F0C0040000000000000002 /* HostedInspectorDockControlScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostedInspectorDockControlScriptTests.swift; sourceTree = "<group>"; };
B77620000000000000000001 /* HostedInspectorResizeGeometry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/HostedInspectorResizeGeometry.swift; sourceTree = "<group>"; };
B77620000000000000000007 /* HostedInspectorResizeGeometryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostedInspectorResizeGeometryTests.swift; sourceTree = "<group>"; };
CD0CFE6300000000CD0CFE63 /* HostSettingsActions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HostSettingsActions.swift; sourceTree = "<group>"; };
F1C1AA20B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactivePaneFirstClickFocusTests.swift; sourceTree = "<group>"; };
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4160,6 +4168,8 @@
109D0E38E29A8779FA0BAE82 /* BrowserRemoteWorkspaceStatus.swift */,
326E7A5E13C6DF650B9F8971 /* BrowserSystemProxyWatcher.swift */,
A5001414 /* BrowserPanelView.swift */,
B77620000000000000000003 /* HostedInspectorAttachedSizeSync.swift */,
B77620000000000000000001 /* HostedInspectorResizeGeometry.swift */,
C67540070000000000000002 /* BrowserPDFDocumentToolbarButtons.swift */,
C42660010000000000000002 /* BrowserPDFPreviewActionUIDelegate.swift */,
C42660020000000000000002 /* BrowserPDFPrintOperationRunner.swift */,
Expand Down Expand Up @@ -4699,6 +4709,8 @@
B1F0C0060000000000000002 /* BrowserDeveloperToolsDockControlNormalizerTests.swift */,
B1F0C0020000000000000002 /* BrowserInspectorFocusHandoffTests.swift */,
B1F0C0040000000000000002 /* HostedInspectorDockControlScriptTests.swift */,
B77620000000000000000005 /* HostedInspectorBottomDockDividerTests.swift */,
B77620000000000000000007 /* HostedInspectorResizeGeometryTests.swift */,
D7032A030000000000000002 /* BrowserClientCertificateCredentialPickerTests.swift */,
BABA25000000000000000006 /* BrowserHTTPBasicAuthPromptCoordinatorTests.swift */,
BABA25000000000000000004 /* BrowserHTTPBasicAuthPromptTests.swift */,
Expand Down Expand Up @@ -5813,7 +5825,9 @@
A6AC72020000000000000001 /* GPUSpinnerStyle.swift in Sources */,
F5320000A1B2C3D4E5F60718 /* HermesAgentIndex.swift in Sources */,
CD0CFE6000000000CD0CFE60 /* HostAccountFlow.swift in Sources */,
B77620000000000000000004 /* HostedInspectorAttachedSizeSync.swift in Sources */,
B1F0C0030000000000000001 /* HostedInspectorDockControlScript.swift in Sources */,
B77620000000000000000002 /* HostedInspectorResizeGeometry.swift in Sources */,
CD0CFE6200000000CD0CFE62 /* HostSettingsActions.swift in Sources */,
C0DEA7720000000000000001 /* InternalFlagsWindow.swift in Sources */,
4D8B37E0E11A29997632765F /* InternalTabDragConfiguration.swift in Sources */,
Expand Down Expand Up @@ -6752,7 +6766,9 @@
D7AB34400000000000000003 /* GhosttyTerminalViewVisibilityPolicyTests.swift in Sources */,
3865A0053865A0053865A005 /* GlobalSearchShortcutSettingsTests.swift in Sources */,
4931A11B0000000000000002 /* HiddenRightSidebarContentMountingTests.swift in Sources */,
B77620000000000000000006 /* HostedInspectorBottomDockDividerTests.swift in Sources */,
B1F0C0040000000000000001 /* HostedInspectorDockControlScriptTests.swift in Sources */,
B77620000000000000000008 /* HostedInspectorResizeGeometryTests.swift in Sources */,
F1C1AA21B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift in Sources */,
C34670050000000000000001 /* KeyboardShortcutContextSwiftTests.swift in Sources */,
C34670020000000000000001 /* KeyboardShortcutContextTests.swift in Sources */,
Expand Down
8 changes: 4 additions & 4 deletions cmuxTests/BrowserPanelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2411,7 +2411,7 @@ final class BrowserPanelHostContainerViewTests: XCTestCase {

func testBrowserPanelHostAllowsRightDockedInspectorToExpandLeftAfterPromotion() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 420, height: 260),
contentRect: NSRect(x: 0, y: 0, width: 640, height: 260),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
Expand All @@ -2422,14 +2422,14 @@ final class BrowserPanelHostContainerViewTests: XCTestCase {
return
}

let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 240, height: contentView.bounds.height))
let host = WebViewRepresentable.HostContainerView(frame: NSRect(x: 180, y: 0, width: 420, 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 pageView = WKWebView(frame: NSRect(x: 0, y: 0, width: 272, height: host.bounds.height))
let inspectorView = WKWebView(
frame: NSRect(x: 92, y: 0, width: slotView.bounds.width - 92, height: host.bounds.height)
frame: NSRect(x: 272, y: 0, width: slotView.bounds.width - 272, height: host.bounds.height)
)
slotView.addSubview(pageView)
slotView.addSubview(inspectorView)
Expand Down
Loading
Loading