Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
12233 Sources/GhosttyTerminalView.swift
11592 Sources/Panels/BrowserPanel.swift
9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8046 Sources/Panels/BrowserPanelView.swift
8016 CLI/cmux_open.swift
7982 Sources/Panels/BrowserPanelView.swift
7366 cmuxTests/WorkspaceUnitTests.swift
7218 cmuxTests/WorkspaceRemoteConnectionTests.swift
6895 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
Expand All @@ -24,7 +24,7 @@
4487 Sources/Panels/FilePreviewPanel.swift
4476 Sources/cmuxApp.swift
4367 cmuxTests/BrowserPanelTests.swift
4133 Sources/BrowserWindowPortal.swift
4121 Sources/BrowserWindowPortal.swift
3934 Sources/Feed/FeedPanelView.swift
3926 cmuxTests/TabManagerUnitTests.swift
3896 cmuxTests/WindowAndDragTests.swift
Expand All @@ -42,7 +42,7 @@
2395 Sources/Mobile/MobileHostService.swift
2328 cmuxTests/CJKIMEInputTests.swift
2242 Sources/TerminalNotificationStore.swift
2233 Sources/TerminalWindowPortal.swift
2231 Sources/TerminalWindowPortal.swift
2126 cmuxTests/CmuxConfigTests.swift
2091 cmuxTests/ShortcutAndCommandPaletteTests.swift
2078 Sources/SessionPersistence.swift
Expand Down
48 changes: 18 additions & 30 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -818,10 +818,18 @@ final class WindowBrowserHostView: NSView {
at point: NSPoint,
visibleSlots: [WindowBrowserSlotView]
) -> Bool {
guard let rightMostEdge = visibleSlots.map(\.frame.maxX).max() else { return false }
let trailingGap = bounds.maxX - rightMostEdge
let dockDividerX = visibleSlots
.filter { $0.isRightSidebarDockSlot }
.map(\.frame.minX)
.min()
let contentRightEdge = visibleSlots
.filter { !$0.isRightSidebarDockSlot }
.map(\.frame.maxX)
.max()
guard let dividerX = dockDividerX ?? contentRightEdge else { return false }
let trailingGap = bounds.maxX - dividerX
guard trailingGap > Self.minimumVisibleLeadingContentWidth else { return false }
return SidebarResizeInteraction.Edge.trailing.hitRange(dividerX: rightMostEdge).contains(point.x)
return SidebarResizeInteraction.Edge.trailing.hitRange(dividerX: dividerX).contains(point.x)
}

private func updateDividerCursor(
Expand Down Expand Up @@ -1155,11 +1163,9 @@ final class WindowBrowserHostView: NSView {
}

private static func dividerHit(at windowPoint: NSPoint, in regions: [DividerRegion], checkLiveness: Bool = true) -> DividerHit? {
let expansion: CGFloat = 5
for region in regions.reversed() {
if checkLiveness, !region.isLive { continue }
let hitRect = region.rectInWindow.insetBy(dx: -expansion, dy: -expansion)
.intersection(region.boundsInWindow)
let hitRect = region.hitRectInWindow
if !hitRect.isNull, hitRect.contains(windowPoint) {
return DividerHit(
kind: region.isVertical ? .vertical : .horizontal,
Expand Down Expand Up @@ -1313,6 +1319,7 @@ final class WindowBrowserSlotView: NSView {
private var paneTopChromeHeight: CGFloat = 0
var preferredHostedInspectorWidth: CGFloat?
private var preferredHostedInspectorWidthFraction: CGFloat?
fileprivate var isRightSidebarDockSlot = false
fileprivate var isHostedInspectorDividerDragActive = false
var onHostedInspectorLayout: ((WindowBrowserSlotView) -> Void)?
fileprivate var isApplyingHostedInspectorLayout = false
Expand Down Expand Up @@ -1400,6 +1407,7 @@ final class WindowBrowserSlotView: NSView {

func setPaneDropContext(_ context: BrowserPaneDropContext?) {
paneDropTargetView.dropContext = context
isRightSidebarDockSlot = context.map { AppDelegate.shared?.dockForPane($0.paneId) != nil } ?? false
}

var currentPaneDropContext: BrowserPaneDropContext? {
Expand Down Expand Up @@ -1974,30 +1982,10 @@ final class WindowBrowserPortal: NSObject {
guard splitView.arrangedSubviews.count >= 2 else { return }

let location = splitView.convert(event.locationInWindow, from: nil)
let first = splitView.arrangedSubviews[0].frame
let second = splitView.arrangedSubviews[1].frame
let thickness = splitView.dividerThickness
let dividerRect: NSRect

if splitView.isVertical {
guard first.width > 1, second.width > 1 else { return }
dividerRect = NSRect(
x: max(0, first.maxX),
y: 0,
width: thickness,
height: splitView.bounds.height
)
} else {
guard first.height > 1, second.height > 1 else { return }
dividerRect = NSRect(
x: 0,
y: max(0, first.maxY),
width: splitView.bounds.width,
height: thickness
)
}

let hitRect = dividerRect.insetBy(dx: -5, dy: -5)
guard let hitRect = PortalSplitDividerRegion.dividerHitRect(
in: splitView,
dividerIndex: 0
) else { return }
if dividerHitRectContains(location, rect: hitRect) {
window.browserPortalHasInteractiveSplitDividerDrag = true
}
Expand Down
64 changes: 64 additions & 0 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6486,6 +6486,12 @@ struct WebViewRepresentable: NSViewRepresentable {
if passThrough {
#if DEBUG
debugLogHitTest(stage: "hitTest.pass", point: point, passThrough: true, hitView: nil)
#endif
return nil
}
if shouldPassThroughToExternalSplitDivider(at: point, hostedInspectorHit: hostedInspectorHit) {
#if DEBUG
debugLogHitTest(stage: "hitTest.splitPass", point: point, passThrough: true, hitView: nil)
#endif
return nil
}
Expand Down Expand Up @@ -6646,6 +6652,60 @@ struct WebViewRepresentable: NSViewRepresentable {
return contentView.bounds.maxX - hostRectInContent.maxX > 24
}

private func shouldPassThroughToExternalSplitDivider(
at point: NSPoint,
hostedInspectorHit: HostedInspectorDividerHit? = nil
) -> Bool {
guard hostedInspectorHit == nil else { return false }
guard isNearPaneEdge(point) else { return false }
guard window != nil else { return false }

let windowPoint = convert(point, to: nil)
var ancestor = superview
while let currentAncestor = ancestor {
if let splitView = currentAncestor as? NSSplitView,
let arrangedIndex = splitView.arrangedSubviews.firstIndex(where: { arrangedSubview in
self === arrangedSubview || self.isDescendant(of: arrangedSubview)
}) {
if externalSplitDividerHit(at: windowPoint, in: splitView, dividerIndex: arrangedIndex - 1) {
return true
}
if externalSplitDividerHit(at: windowPoint, in: splitView, dividerIndex: arrangedIndex) {
return true
}
}
ancestor = currentAncestor.superview
}
return false
}

private func externalSplitDividerHit(
at windowPoint: NSPoint,
in splitView: NSSplitView,
dividerIndex: Int
) -> Bool {
guard let window,
splitView.window === window,
let hitRect = PortalSplitDividerRegion.dividerHitRectInWindow(
in: splitView,
dividerIndex: dividerIndex
) else {
return false
}
return hitRect.contains(windowPoint)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

private func isNearPaneEdge(_ point: NSPoint) -> Bool {
// hitTest and cursor tracking call this with points in this view's bounds coordinates.
guard bounds.contains(point) else { return false }
let expansion = PortalSplitDividerRegion.dividerHitExpansion
let nearVerticalEdge = point.x <= bounds.minX + expansion ||
point.x >= bounds.maxX - expansion
let nearHorizontalEdge = point.y <= bounds.minY + expansion ||
point.y >= bounds.maxY - expansion
return nearVerticalEdge || nearHorizontalEdge
}
Comment on lines +6698 to +6707

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 isNearPaneEdge compares hitTest point in superview coordinates against bounds in local coordinates

NSView.hitTest(_:) receives point in the superview's coordinate system, but bounds.minX/bounds.maxX/bounds.minY/bounds.maxY are in the view's own coordinate system. The check works today because HostContainerView is always pinned to origin (0,0) inside its container (so superview coordinates coincide with local coordinates), but the assumption is invisible. If the view is ever laid out with a non-zero frame origin the near-edge predicate would silently misfire, causing either spurious pass-throughs or missed ones.

Consider converting the point to local coordinates first (let local = superview?.convert(point, to: self) ?? point) and comparing against bounds, or adding a comment that documents the origin-is-zero invariant.

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.

Addressed in f87f8e6: now documents and guards the local-bounds coordinate invariant, and mirrors the external split-divider pass-through path with so the underlying split cursor can take over.

— Claude Code

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.

Correction to the previous reply: addressed in f87f8e6. isNearPaneEdge now documents and guards the local-bounds coordinate invariant, and updateDividerCursor mirrors the external split-divider pass-through path with restoreArrow false so the underlying split cursor can take over.

— Claude Code


private func updateDividerCursor(
at point: NSPoint,
hostedInspectorHit: HostedInspectorDividerHit? = nil
Expand All @@ -6655,6 +6715,10 @@ struct WebViewRepresentable: NSViewRepresentable {
clearActiveDividerCursor(restoreArrow: false)
return
}
if shouldPassThroughToExternalSplitDivider(at: point, hostedInspectorHit: resolvedHostedInspectorHit) {
clearActiveDividerCursor(restoreArrow: false)
return
}
guard resolvedHostedInspectorHit != nil else {
clearActiveDividerCursor(restoreArrow: true)
return
Expand Down
54 changes: 41 additions & 13 deletions Sources/PortalSplitDividerRegion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ final class PortalSplitDividerRegion {
let isVertical: Bool
let isInHostedContent: Bool

static let dividerHitExpansion: CGFloat = 5

init(
splitView: NSSplitView,
dividerIndex: Int,
Expand Down Expand Up @@ -52,6 +54,44 @@ final class PortalSplitDividerRegion {
regions.allSatisfy(\.isLive)
}

var hitRectInWindow: NSRect {
rectInWindow
.insetBy(dx: -Self.dividerHitExpansion, dy: -Self.dividerHitExpansion)
.intersection(boundsInWindow)
}

static func dividerRect(in splitView: NSSplitView, dividerIndex: Int) -> NSRect? {
guard dividerIndex >= 0,
dividerIndex + 1 < splitView.arrangedSubviews.count else {
return nil
}

let first = splitView.arrangedSubviews[dividerIndex].frame
let second = splitView.arrangedSubviews[dividerIndex + 1].frame
let thickness = splitView.dividerThickness
if splitView.isVertical {
guard first.width > 1 || second.width > 1 else { return nil }
return NSRect(x: max(0, first.maxX), y: 0, width: thickness, height: splitView.bounds.height)
}

guard first.height > 1 || second.height > 1 else { return nil }
return NSRect(x: 0, y: max(0, first.maxY), width: splitView.bounds.width, height: thickness)
}

static func dividerHitRect(in splitView: NSSplitView, dividerIndex: Int) -> NSRect? {
guard let dividerRect = dividerRect(in: splitView, dividerIndex: dividerIndex) else { return nil }
return dividerRect
.insetBy(dx: -Self.dividerHitExpansion, dy: -Self.dividerHitExpansion)
.intersection(splitView.bounds)
}

static func dividerHitRectInWindow(in splitView: NSSplitView, dividerIndex: Int) -> NSRect? {
guard let hitRect = dividerHitRect(in: splitView, dividerIndex: dividerIndex) else { return nil }
let hitRectInWindow = splitView.convert(hitRect, to: nil)
guard hitRectInWindow.width > 0, hitRectInWindow.height > 0 else { return nil }
return hitRectInWindow
}

static func collect(
in rootView: NSView,
hostView: NSView? = nil
Expand Down Expand Up @@ -142,19 +182,7 @@ final class PortalSplitDividerRegion {
let splitBoundsInWindow = splitView.convert(splitView.bounds, to: nil)
let dividerCount = max(0, splitView.arrangedSubviews.count - 1)
for dividerIndex in 0..<dividerCount {
let first = splitView.arrangedSubviews[dividerIndex].frame
let second = splitView.arrangedSubviews[dividerIndex + 1].frame
let thickness = splitView.dividerThickness
let dividerRect: NSRect
if splitView.isVertical {
guard first.width > 1 || second.width > 1 else { continue }
let x = max(0, first.maxX)
dividerRect = NSRect(x: x, y: 0, width: thickness, height: splitView.bounds.height)
} else {
guard first.height > 1 || second.height > 1 else { continue }
let y = max(0, first.maxY)
dividerRect = NSRect(x: 0, y: y, width: splitView.bounds.width, height: thickness)
}
guard let dividerRect = dividerRect(in: splitView, dividerIndex: dividerIndex) else { continue }
let dividerRectInWindow = splitView.convert(dividerRect, to: nil)
guard dividerRectInWindow.width > 0, dividerRectInWindow.height > 0 else { continue }
result.append(PortalSplitDividerRegion(
Expand Down
4 changes: 1 addition & 3 deletions Sources/TerminalWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -417,11 +417,9 @@ final class WindowTerminalHostView: NSView {
}

private static func dividerCursorKind(at windowPoint: NSPoint, in regions: [DividerRegion], checkLiveness: Bool = true) -> DividerCursorKind? {
let expansion: CGFloat = 5
for region in regions.reversed() {
if checkLiveness, !region.isLive { continue }
let hitRect = region.rectInWindow.insetBy(dx: -expansion, dy: -expansion)
.intersection(region.boundsInWindow)
let hitRect = region.hitRectInWindow
if !hitRect.isNull, hitRect.contains(windowPoint) {
return region.isVertical ? .vertical : .horizontal
}
Expand Down
Loading