Skip to content

Commit fd279bd

Browse files
authored
Fix splitter hitbox overlap and terminal scrollbar width resync (#1950)
* test: add splitter and scrollbar regressions * fix: narrow sidebar overlap and resync terminal width * test: unwrap pending surface width in scrollbar regression * fix: restore hosted inspector divider drag path
1 parent 5ced313 commit fd279bd

7 files changed

Lines changed: 283 additions & 39 deletions

Sources/BrowserWindowPortal.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,8 +732,8 @@ final class WindowBrowserHostView: NSView {
732732
return false
733733
}
734734

735-
let regionMinX = dividerX - SidebarResizeInteraction.hitWidthPerSide
736-
let regionMaxX = dividerX + SidebarResizeInteraction.hitWidthPerSide
735+
let regionMinX = dividerX - SidebarResizeInteraction.sidebarSideHitWidth
736+
let regionMaxX = dividerX + SidebarResizeInteraction.contentSideHitWidth
737737
return point.x >= regionMinX && point.x <= regionMaxX
738738
}
739739

Sources/ContentView.swift

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -336,11 +336,13 @@ final class SidebarState: ObservableObject {
336336
}
337337

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

342-
static var hitWidthPerSide: CGFloat {
343-
hitInset + (handleWidth / 2)
344+
static var totalHitWidth: CGFloat {
345+
sidebarSideHitWidth + contentSideHitWidth
344346
}
345347
}
346348

@@ -2025,8 +2027,12 @@ struct ContentView: View {
20252027
case divider
20262028
}
20272029

2028-
private var sidebarResizerHitWidthPerSide: CGFloat {
2029-
SidebarResizeInteraction.hitWidthPerSide
2030+
private var sidebarResizerSidebarHitWidth: CGFloat {
2031+
SidebarResizeInteraction.sidebarSideHitWidth
2032+
}
2033+
2034+
private var sidebarResizerContentHitWidth: CGFloat {
2035+
SidebarResizeInteraction.contentSideHitWidth
20302036
}
20312037

20322038
private func maxSidebarWidth(availableWidth: CGFloat? = nil) -> CGFloat {
@@ -2102,8 +2108,8 @@ struct ContentView: View {
21022108

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

@@ -2283,7 +2289,7 @@ struct ContentView: View {
22832289
GeometryReader { proxy in
22842290
let totalWidth = max(0, proxy.size.width)
22852291
let dividerX = min(max(sidebarWidth, 0), totalWidth)
2286-
let leadingWidth = max(0, dividerX - sidebarResizerHitWidthPerSide)
2292+
let leadingWidth = max(0, dividerX - sidebarResizerSidebarHitWidth)
22872293

22882294
HStack(spacing: 0) {
22892295
Color.clear
@@ -2292,7 +2298,7 @@ struct ContentView: View {
22922298

22932299
sidebarResizerHandleOverlay(
22942300
.divider,
2295-
width: sidebarResizerHitWidthPerSide * 2,
2301+
width: SidebarResizeInteraction.totalHitWidth,
22962302
availableWidth: totalWidth,
22972303
accessibilityIdentifier: "SidebarResizer"
22982304
)

Sources/GhosttyTerminalView.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4468,6 +4468,12 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
44684468
updateSurfaceSize(size: size)
44694469
}
44704470

4471+
#if DEBUG
4472+
fileprivate func debugPendingSurfaceSize() -> CGSize? {
4473+
pendingSurfaceSize
4474+
}
4475+
#endif
4476+
44714477
/// Force a full size reconciliation for the current bounds.
44724478
/// Keep the drawable-size cache intact so redundant refresh paths do not
44734479
/// reallocate Metal drawables when the pixel size is unchanged.
@@ -7032,6 +7038,17 @@ final class GhosttySurfaceScrollView: NSView {
70327038
) { [weak self] _ in
70337039
self?.synchronizeScrollView()
70347040
})
7041+
7042+
observers.append(NotificationCenter.default.addObserver(
7043+
forName: NSScroller.preferredScrollerStyleDidChangeNotification,
7044+
object: nil,
7045+
// Match AppKit's geometry change immediately so the terminal width
7046+
// does not stay stuck behind a legacy scrollbar gutter.
7047+
queue: nil
7048+
) { [weak self] _ in
7049+
self?.handlePreferredScrollerStyleChange()
7050+
})
7051+
70357052
}
70367053

70377054
required init?(coder: NSCoder) {
@@ -8061,6 +8078,10 @@ final class GhosttySurfaceScrollView: NSView {
80618078
surfaceView.debugSimulateFileDrop(paths: paths)
80628079
}
80638080

8081+
func debugPendingSurfaceSize() -> CGSize? {
8082+
surfaceView.debugPendingSurfaceSize()
8083+
}
8084+
80648085
func debugRegisteredDropTypes() -> [String] {
80658086
surfaceView.debugRegisteredDropTypes()
80668087
}
@@ -9072,6 +9093,21 @@ final class GhosttySurfaceScrollView: NSView {
90729093
synchronizeScrollView()
90739094
}
90749095

9096+
private func handlePreferredScrollerStyleChange() {
9097+
guard Thread.isMainThread else {
9098+
DispatchQueue.main.async { [weak self] in
9099+
self?.handlePreferredScrollerStyleChange()
9100+
}
9101+
return
9102+
}
9103+
9104+
// Retile just the scroll view so contentSize reflects the current
9105+
// scrollbar mode without perturbing viewport origin or hosted view
9106+
// geometry; the broader reconcile path caused visible content glitches.
9107+
scrollView.tile()
9108+
_ = synchronizeCoreSurface()
9109+
}
9110+
90759111
private func documentHeight() -> CGFloat {
90769112
let contentHeight = scrollView.contentSize.height
90779113
let cellHeight = surfaceView.cellSize.height

Sources/Panels/BrowserPanelView.swift

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5037,10 +5037,12 @@ struct WebViewRepresentable: NSViewRepresentable {
50375037
// Origin-only frame churn is common while the surrounding split layout
50385038
// settles. Reapplying the side-docked inspector at the same size fights
50395039
// WebKit's own dock layout and shows up as visible flicker.
5040-
if !isHostedInspectorSideDockActive() &&
5041-
!isHostedInspectorDividerDragActive &&
5042-
!hasStoredHostedInspectorWidthPreference {
5043-
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout.sameSize")
5040+
if !isHostedInspectorDividerDragActive {
5041+
if hasStoredHostedInspectorWidthPreference {
5042+
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "host.layout.sameSize")
5043+
} else if !isHostedInspectorSideDockActive() {
5044+
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout.sameSize")
5045+
}
50445046
}
50455047
updateHostedInspectorDockControlAvailabilityIfNeeded(reason: "host.layout.sameSize")
50465048
notifyGeometryChangedIfNeeded()
@@ -5052,7 +5054,9 @@ struct WebViewRepresentable: NSViewRepresentable {
50525054
lastHostedInspectorLayoutBoundsSize = bounds.size
50535055
if isHostedInspectorSideDockActive() {
50545056
layoutHostedInspectorSideDockIfNeeded(reason: "host.layout.sideDock")
5055-
} else if !hasStoredHostedInspectorWidthPreference {
5057+
} else if hasStoredHostedInspectorWidthPreference {
5058+
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "host.layout")
5059+
} else {
50565060
captureHostedInspectorPreferredWidthFromCurrentLayout(reason: "host.layout")
50575061
}
50585062
updateHostedInspectorDockControlAvailabilityIfNeeded(reason: "host.layout")
@@ -5130,26 +5134,24 @@ struct WebViewRepresentable: NSViewRepresentable {
51305134
return nil
51315135
}
51325136
if let hostedInspectorHit {
5133-
let isSideDockHit = isHostedInspectorSideDockHit(hostedInspectorHit)
51345137
if let nativeHit = nativeHostedInspectorHit(at: point, hostedInspectorHit: hostedInspectorHit) {
51355138
#if DEBUG
51365139
debugLogHitTest(stage: "hitTest.hostedInspectorNative", point: point, passThrough: false, hitView: nativeHit)
51375140
#endif
5138-
if !isSideDockHit ||
5139-
(nativeHit !== hostedInspectorHit.inspectorView &&
5140-
!hostedInspectorHit.inspectorView.isDescendant(of: nativeHit)) {
5141+
if nativeHit !== hostedInspectorHit.inspectorView &&
5142+
!hostedInspectorHit.inspectorView.isDescendant(of: nativeHit) {
51415143
return nativeHit
51425144
}
51435145
}
51445146
#if DEBUG
51455147
debugLogHitTest(
5146-
stage: isSideDockHit ? "hitTest.hostedInspectorManual" : "hitTest.hostedInspectorFallback",
5148+
stage: "hitTest.hostedInspectorManual",
51475149
point: point,
51485150
passThrough: false,
5149-
hitView: hostedInspectorHit.inspectorView
5151+
hitView: self
51505152
)
51515153
#endif
5152-
return isSideDockHit ? self : hostedInspectorHit.inspectorView
5154+
return self
51535155
}
51545156
let hit = super.hitTest(point)
51555157
#if DEBUG
@@ -5160,8 +5162,7 @@ struct WebViewRepresentable: NSViewRepresentable {
51605162

51615163
override func mouseDown(with event: NSEvent) {
51625164
let point = convert(event.locationInWindow, from: nil)
5163-
guard let hostedInspectorHit = hostedInspectorDividerHit(at: point),
5164-
isHostedInspectorSideDockHit(hostedInspectorHit) else {
5165+
guard let hostedInspectorHit = hostedInspectorDividerHit(at: point) else {
51655166
super.mouseDown(with: event)
51665167
return
51675168
}
@@ -5258,7 +5259,7 @@ struct WebViewRepresentable: NSViewRepresentable {
52585259
)
52595260
)
52605261
#endif
5261-
layoutHostedInspectorSideDockIfNeeded(reason: "drag.end")
5262+
reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "drag.end")
52625263
}
52635264
super.mouseUp(with: event)
52645265
}
@@ -5273,7 +5274,7 @@ struct WebViewRepresentable: NSViewRepresentable {
52735274
// Pass through a narrow leading-edge band so the shared sidebar divider
52745275
// handle can receive hover/click even when WKWebView is attached here.
52755276
// Keeping this deterministic avoids flicker from dynamic left-edge scans.
5276-
guard point.x >= 0, point.x <= SidebarResizeInteraction.hitWidthPerSide else {
5277+
guard point.x >= 0, point.x <= SidebarResizeInteraction.contentSideHitWidth else {
52775278
return false
52785279
}
52795280
guard let window, let contentView = window.contentView else {
@@ -5495,9 +5496,9 @@ struct WebViewRepresentable: NSViewRepresentable {
54955496
guard let self else { return }
54965497
self.hostedInspectorReapplyWorkItem = nil
54975498
_ = self.promoteHostedInspectorSideDockFromCurrentLayoutIfNeeded()
5498-
if self.isHostedInspectorSideDockActive() {
5499+
if self.hasStoredHostedInspectorWidthPreference {
54995500
self.reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: reason)
5500-
} else if !self.hasStoredHostedInspectorWidthPreference {
5501+
} else {
55015502
self.captureHostedInspectorPreferredWidthFromCurrentLayout(reason: reason)
55025503
}
55035504
}
@@ -5558,7 +5559,6 @@ struct WebViewRepresentable: NSViewRepresentable {
55585559
private func reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: String) {
55595560
guard !isApplyingHostedInspectorLayout else { return }
55605561
guard let hit = hostedInspectorDividerCandidate() else { return }
5561-
guard isHostedInspectorSideDockHit(hit) else { return }
55625562
guard let preferredWidth = resolvedPreferredHostedInspectorWidth(in: hit.containerView.bounds) else {
55635563
return
55645564
}

Sources/TerminalWindowPortal.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,8 @@ final class WindowTerminalHostView: NSView {
243243
return false
244244
}
245245

246-
let regionMinX = dividerX - SidebarResizeInteraction.hitWidthPerSide
247-
let regionMaxX = dividerX + SidebarResizeInteraction.hitWidthPerSide
246+
let regionMinX = dividerX - SidebarResizeInteraction.sidebarSideHitWidth
247+
let regionMaxX = dividerX + SidebarResizeInteraction.contentSideHitWidth
248248
return point.x >= regionMinX && point.x <= regionMaxX
249249
}
250250

cmuxTests/BrowserPanelTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,7 @@ final class WindowBrowserHostViewTests: XCTestCase {
547547

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

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

908-
XCTAssertLessThanOrEqual(dividerPointInHost.x - slot.frame.minX, SidebarResizeInteraction.hitWidthPerSide)
908+
XCTAssertLessThanOrEqual(dividerPointInHost.x - slot.frame.minX, 2)
909909
let dividerHit = host.hitTest(dividerPointInHost)
910910
XCTAssertTrue(
911911
isInspectorOwnedHit(dividerHit, inspectorView: inspectorView, pageView: pageView),

0 commit comments

Comments
 (0)