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
4 changes: 2 additions & 2 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -732,8 +732,8 @@ final class WindowBrowserHostView: NSView {
return false
}

let regionMinX = dividerX - SidebarResizeInteraction.hitWidthPerSide
let regionMaxX = dividerX + SidebarResizeInteraction.hitWidthPerSide
let regionMinX = dividerX - SidebarResizeInteraction.sidebarSideHitWidth
let regionMaxX = dividerX + SidebarResizeInteraction.contentSideHitWidth
return point.x >= regionMinX && point.x <= regionMaxX
}

Expand Down
26 changes: 16 additions & 10 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,13 @@ final class SidebarState: ObservableObject {
}

enum SidebarResizeInteraction {
static let handleWidth: CGFloat = 6
static let hitInset: CGFloat = 3
// Keep a generous drag target inside the sidebar itself, but make the
// terminal-side overlap very small so column-0 text selection still wins.
static let sidebarSideHitWidth: CGFloat = 6
static let contentSideHitWidth: CGFloat = 2

static var hitWidthPerSide: CGFloat {
hitInset + (handleWidth / 2)
static var totalHitWidth: CGFloat {
sidebarSideHitWidth + contentSideHitWidth
}
}

Expand Down Expand Up @@ -1788,8 +1790,12 @@ struct ContentView: View {
case divider
}

private var sidebarResizerHitWidthPerSide: CGFloat {
SidebarResizeInteraction.hitWidthPerSide
private var sidebarResizerSidebarHitWidth: CGFloat {
SidebarResizeInteraction.sidebarSideHitWidth
}

private var sidebarResizerContentHitWidth: CGFloat {
SidebarResizeInteraction.contentSideHitWidth
}

private func maxSidebarWidth(availableWidth: CGFloat? = nil) -> CGFloat {
Expand Down Expand Up @@ -1865,8 +1871,8 @@ struct ContentView: View {

private func dividerBandContains(pointInContent point: NSPoint, contentBounds: NSRect) -> Bool {
guard point.y >= contentBounds.minY, point.y <= contentBounds.maxY else { return false }
let minX = sidebarWidth - sidebarResizerHitWidthPerSide
let maxX = sidebarWidth + sidebarResizerHitWidthPerSide
let minX = sidebarWidth - sidebarResizerSidebarHitWidth
let maxX = sidebarWidth + sidebarResizerContentHitWidth
return point.x >= minX && point.x <= maxX
}

Expand Down Expand Up @@ -2046,7 +2052,7 @@ struct ContentView: View {
GeometryReader { proxy in
let totalWidth = max(0, proxy.size.width)
let dividerX = min(max(sidebarWidth, 0), totalWidth)
let leadingWidth = max(0, dividerX - sidebarResizerHitWidthPerSide)
let leadingWidth = max(0, dividerX - sidebarResizerSidebarHitWidth)

HStack(spacing: 0) {
Color.clear
Expand All @@ -2055,7 +2061,7 @@ struct ContentView: View {

sidebarResizerHandleOverlay(
.divider,
width: sidebarResizerHitWidthPerSide * 2,
width: SidebarResizeInteraction.totalHitWidth,
availableWidth: totalWidth,
accessibilityIdentifier: "SidebarResizer"
)
Expand Down
36 changes: 36 additions & 0 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4454,6 +4454,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
updateSurfaceSize(size: size)
}

#if DEBUG
fileprivate func debugPendingSurfaceSize() -> CGSize? {
pendingSurfaceSize
}
#endif

/// Force a full size reconciliation for the current bounds.
/// Keep the drawable-size cache intact so redundant refresh paths do not
/// reallocate Metal drawables when the pixel size is unchanged.
Expand Down Expand Up @@ -6998,6 +7004,17 @@ final class GhosttySurfaceScrollView: NSView {
) { [weak self] _ in
self?.synchronizeScrollView()
})

observers.append(NotificationCenter.default.addObserver(
forName: NSScroller.preferredScrollerStyleDidChangeNotification,
object: nil,
// Match AppKit's geometry change immediately so the terminal width
// does not stay stuck behind a legacy scrollbar gutter.
queue: nil
) { [weak self] _ in
self?.handlePreferredScrollerStyleChange()
})

}

required init?(coder: NSCoder) {
Expand Down Expand Up @@ -8024,6 +8041,10 @@ final class GhosttySurfaceScrollView: NSView {
surfaceView.debugSimulateFileDrop(paths: paths)
}

func debugPendingSurfaceSize() -> CGSize? {
Comment on lines 8041 to +8044
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.

P0 #if DEBUG guard missing on public wrapper

GhosttySurfaceScrollView.debugPendingSurfaceSize() calls surfaceView.debugPendingSurfaceSize(), which is defined only inside a #if DEBUG block on GhosttyNSView. In a release build, the GhosttyNSView method is stripped, so this wrapper will fail to compile with "Value of type 'GhosttyNSView' has no member 'debugPendingSurfaceSize'".

Suggested change
surfaceView.debugSimulateFileDrop(paths: paths)
}
func debugPendingSurfaceSize() -> CGSize? {
#if DEBUG
func debugPendingSurfaceSize() -> CGSize? {
surfaceView.debugPendingSurfaceSize()
}
#endif

surfaceView.debugPendingSurfaceSize()
}

func debugRegisteredDropTypes() -> [String] {
surfaceView.debugRegisteredDropTypes()
}
Expand Down Expand Up @@ -9029,6 +9050,21 @@ final class GhosttySurfaceScrollView: NSView {
synchronizeScrollView()
}

private func handlePreferredScrollerStyleChange() {
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in
self?.handlePreferredScrollerStyleChange()
}
return
}

// Retile just the scroll view so contentSize reflects the current
// scrollbar mode without perturbing viewport origin or hosted view
// geometry; the broader reconcile path caused visible content glitches.
scrollView.tile()
_ = synchronizeCoreSurface()
}

private func documentHeight() -> CGFloat {
let contentHeight = scrollView.contentSize.height
let cellHeight = surfaceView.cellSize.height
Expand Down
38 changes: 19 additions & 19 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5034,10 +5034,12 @@ struct WebViewRepresentable: NSViewRepresentable {
// 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")
if !isHostedInspectorDividerDragActive {
if hasStoredHostedInspectorWidthPreference {
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "host.layout.sameSize")
} else if !isHostedInspectorSideDockActive() {
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout.sameSize")
}
}
updateHostedInspectorDockControlAvailabilityIfNeeded(reason: "host.layout.sameSize")
notifyGeometryChangedIfNeeded()
Expand All @@ -5049,7 +5051,9 @@ struct WebViewRepresentable: NSViewRepresentable {
lastHostedInspectorLayoutBoundsSize = bounds.size
if isHostedInspectorSideDockActive() {
layoutHostedInspectorSideDockIfNeeded(reason: "host.layout.sideDock")
} else if !hasStoredHostedInspectorWidthPreference {
} else if hasStoredHostedInspectorWidthPreference {
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "host.layout")
} else {
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout")
}
updateHostedInspectorDockControlAvailabilityIfNeeded(reason: "host.layout")
Expand Down Expand Up @@ -5127,26 +5131,24 @@ 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
if !isSideDockHit ||
(nativeHit !== hostedInspectorHit.inspectorView &&
!hostedInspectorHit.inspectorView.isDescendant(of: nativeHit)) {
if nativeHit !== hostedInspectorHit.inspectorView &&
!hostedInspectorHit.inspectorView.isDescendant(of: nativeHit) {
return nativeHit
}
}
#if DEBUG
debugLogHitTest(
stage: isSideDockHit ? "hitTest.hostedInspectorManual" : "hitTest.hostedInspectorFallback",
stage: "hitTest.hostedInspectorManual",
point: point,
passThrough: false,
hitView: hostedInspectorHit.inspectorView
hitView: self
)
#endif
return isSideDockHit ? self : hostedInspectorHit.inspectorView
return self
}
Comment on lines +5138 to 5152
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In AppKit, when NSView.hitTest(_:)returns a container view instead of the underlying descendant, arerightMouseDown(with:)andotherMouseDown(with:) dispatched to that container?

💡 Result:

Yes, when NSView.hitTest(_:) returns a container view instead of a descendant view, the rightMouseDown(with:) and otherMouseDown(with:) events are dispatched to that container view.

Citations:


🏁 Script executed:

# First, verify the file exists and examine the code around lines 5138-5152
wc -l Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 102


🏁 Script executed:

# Read the specific section mentioned in the review
sed -n '5130,5160p' Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 1177


🏁 Script executed:

# Find HostContainerView class to check mouse event handlers
rg -A 20 "class HostContainerView" Sources/Panels/

Repository: manaflow-ai/cmux

Length of output: 1697


🏁 Script executed:

# Get the full HostContainerView class implementation with all methods
rg -A 200 "^\s*final class HostContainerView: NSView" Sources/Panels/BrowserPanelView.swift | head -300

Repository: manaflow-ai/cmux

Length of output: 10329


🏁 Script executed:

# More specific search for mouse event handlers in HostContainerView
rg "(mouseDown|rightMouseDown|otherMouseDown|mouseMoved)" Sources/Panels/BrowserPanelView.swift | grep -A 2 -B 2 "HostContainerView\|func.*[mM]ouse"

Repository: manaflow-ai/cmux

Length of output: 511


🏁 Script executed:

# Search for all methods in HostContainerView to understand its structure
ast-grep --pattern 'final class HostContainerView {
  $$$
}' | head -100

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Search for mouse event handlers specifically in HostContainerView
rg -n "override func (mouseDown|rightMouseDown|otherMouseDown|mouseMoved)" Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 236


🏁 Script executed:

# Get the full HostContainerView class boundaries to understand its scope
rg -n "^\s*final class HostContainerView|^    }" Sources/Panels/BrowserPanelView.swift | head -20

Repository: manaflow-ai/cmux

Length of output: 255


🏁 Script executed:

# Find the exact line where HostContainerView is defined and where it ends
rg -n "final class HostContainerView" Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 108


🏁 Script executed:

# Now find the closing brace of HostContainerView class by searching for its extent
sed -n '4705,4750p' Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 2276


🏁 Script executed:

# Get a better view - search for where HostContainerView class definition starts
grep -n "final class HostContainerView" Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 108


🏁 Script executed:

# Now that we know HostContainerView likely starts around line 4705+, let's find mouse handlers around that area
sed -n '5150,5180p' Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 1354


🏁 Script executed:

# Search for rightMouseDown and otherMouseDown in entire HostContainerView class
# We know it starts at line 4287, so let's search after that line
rg -n "override func (rightMouseDown|otherMouseDown)" Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

# Verify the mouseMoved handler around line 5115 is in HostContainerView
sed -n '5110,5125p' Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 699


🏁 Script executed:

# Get a complete picture - show all override methods in HostContainerView starting from line 4287
sed -n '4287,6392p' Sources/Panels/BrowserPanelView.swift | grep -n "override func" | head -20

Repository: manaflow-ai/cmux

Length of output: 922


🏁 Script executed:

# Final confirmation - view the exact hitTest logic one more time to ensure understanding
sed -n '5115,5160p' Sources/Panels/BrowserPanelView.swift

Repository: manaflow-ai/cmux

Length of output: 1923


Gate divider hits to left-button events only.

hitTest(_:) unconditionally returns self for hosted-inspector divider hits, but HostContainerView only implements mouseDown, mouseDragged, and mouseUp handlers—not rightMouseDown or otherMouseDown. Secondary-button clicks in the divider band will dispatch to the host container instead of reaching WebKit/Inspector.

Add an event-type check before accepting the divider hit:

Suggested adjustment
-            if let hostedInspectorHit {
-                if let nativeHit = nativeHostedInspectorHit(at: point, hostedInspectorHit: hostedInspectorHit) {
+            if let hostedInspectorHit {
+                let nativeHit = nativeHostedInspectorHit(at: point, hostedInspectorHit: hostedInspectorHit)
+                if let nativeHit {
 `#if` DEBUG
                     debugLogHitTest(stage: "hitTest.hostedInspectorNative", point: point, passThrough: false, hitView: nativeHit)
 `#endif`
                     if nativeHit !== hostedInspectorHit.inspectorView &&
                         !hostedInspectorHit.inspectorView.isDescendant(of: nativeHit) {
                         return nativeHit
                     }
                 }
+                let shouldUseManualDividerHit: Bool = {
+                    switch NSApp.currentEvent?.type {
+                    case .leftMouseDown, .leftMouseDragged, .leftMouseUp:
+                        return true
+                    default:
+                        return false
+                    }
+                }()
+                guard shouldUseManualDividerHit else {
+                    return nativeHit ?? super.hitTest(point)
+                }
 `#if` DEBUG
                 debugLogHitTest(
                     stage: "hitTest.hostedInspectorManual",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/BrowserPanelView.swift` around lines 5138 - 5152, The divider
hit path in hitTest(_:) currently always returns self causing right/other mouse
events to be handled by HostContainerView; change the end of that branch to
inspect the current NSEvent (via NSApp.currentEvent) and only accept the
hosted-inspector divider hit (return self) when the event type is a left-button
event (e.g. .leftMouseDown, .leftMouseDragged, .leftMouseUp); for any other
event types, return nativeHit so the event continues to WebKit/Inspector. Ensure
this check is applied just before the debugLogHitTest/return self block and use
the existing variables hostedInspectorHit and nativeHit to decide the fallback.

let hit = super.hitTest(point)
#if DEBUG
Expand All @@ -5157,8 +5159,7 @@ struct WebViewRepresentable: NSViewRepresentable {

override func mouseDown(with event: NSEvent) {
let point = convert(event.locationInWindow, from: nil)
guard let hostedInspectorHit = hostedInspectorDividerHit(at: point),
isHostedInspectorSideDockHit(hostedInspectorHit) else {
guard let hostedInspectorHit = hostedInspectorDividerHit(at: point) else {
super.mouseDown(with: event)
return
}
Expand Down Expand Up @@ -5255,7 +5256,7 @@ struct WebViewRepresentable: NSViewRepresentable {
)
)
#endif
layoutHostedInspectorSideDockIfNeeded(reason: "drag.end")
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "drag.end")
}
super.mouseUp(with: event)
}
Expand All @@ -5270,7 +5271,7 @@ struct WebViewRepresentable: NSViewRepresentable {
// Pass through a narrow leading-edge band so the shared sidebar divider
// handle can receive hover/click even when WKWebView is attached here.
// Keeping this deterministic avoids flicker from dynamic left-edge scans.
guard point.x >= 0, point.x <= SidebarResizeInteraction.hitWidthPerSide else {
guard point.x >= 0, point.x <= SidebarResizeInteraction.contentSideHitWidth else {
return false
}
guard let window, let contentView = window.contentView else {
Expand Down Expand Up @@ -5492,9 +5493,9 @@ struct WebViewRepresentable: NSViewRepresentable {
guard let self else { return }
self.hostedInspectorReapplyWorkItem = nil
_ = self.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded()
if self.isHostedInspectorSideDockActive() {
if self.hasStoredHostedInspectorWidthPreference {
self.reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: reason)
} else if !self.hasStoredHostedInspectorWidthPreference {
} else {
self.captureHostedInspectorPreferredWidthFromCurrentLayout(reason: reason)
}
}
Expand Down Expand Up @@ -5555,7 +5556,6 @@ 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
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/TerminalWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ final class WindowTerminalHostView: NSView {
return false
}

let regionMinX = dividerX - SidebarResizeInteraction.hitWidthPerSide
let regionMaxX = dividerX + SidebarResizeInteraction.hitWidthPerSide
let regionMinX = dividerX - SidebarResizeInteraction.sidebarSideHitWidth
let regionMaxX = dividerX + SidebarResizeInteraction.contentSideHitWidth
return point.x >= regionMinX && point.x <= regionMaxX
}

Expand Down
4 changes: 2 additions & 2 deletions cmuxTests/BrowserPanelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ final class WindowBrowserHostViewTests: XCTestCase {

XCTAssertLessThanOrEqual(inspectorSplit.arrangedSubviews[0].frame.width, 1.5)
XCTAssertTrue(
abs(dividerPointInHost.x - slot.frame.minX) <= SidebarResizeInteraction.hitWidthPerSide,
abs(dividerPointInHost.x - slot.frame.minX) <= 2,
"Expected collapsed hosted divider to overlap the browser slot leading-edge resizer zone"
)

Expand Down Expand Up @@ -905,7 +905,7 @@ final class WindowBrowserHostViewTests: XCTestCase {
let dividerPointInWindow = slot.convert(dividerPointInSlot, to: nil)
let dividerPointInHost = host.convert(dividerPointInWindow, from: nil)

XCTAssertLessThanOrEqual(dividerPointInHost.x - slot.frame.minX, SidebarResizeInteraction.hitWidthPerSide)
XCTAssertLessThanOrEqual(dividerPointInHost.x - slot.frame.minX, 2)
let dividerHit = host.hitTest(dividerPointInHost)
XCTAssertTrue(
isInspectorOwnedHit(dividerHit, inspectorView: inspectorView, pageView: pageView),
Expand Down
Loading
Loading