From 15901940ae07493e686c893db967cf9a51dc5efb Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 21 Mar 2026 15:15:10 -0700 Subject: [PATCH 1/4] test: add splitter and scrollbar regressions --- Sources/GhosttyTerminalView.swift | 11 ++ cmuxTests/BrowserPanelTests.swift | 4 +- cmuxTests/TerminalAndGhosttyTests.swift | 158 ++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 2ee395f0ca..66c6b04e4e 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -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. @@ -6998,6 +7004,7 @@ final class GhosttySurfaceScrollView: NSView { ) { [weak self] _ in self?.synchronizeScrollView() }) + } required init?(coder: NSCoder) { @@ -8024,6 +8031,10 @@ final class GhosttySurfaceScrollView: NSView { surfaceView.debugSimulateFileDrop(paths: paths) } + func debugPendingSurfaceSize() -> CGSize? { + surfaceView.debugPendingSurfaceSize() + } + func debugRegisteredDropTypes() -> [String] { surfaceView.debugRegisteredDropTypes() } diff --git a/cmuxTests/BrowserPanelTests.swift b/cmuxTests/BrowserPanelTests.swift index 5c61be3d63..65db2bc531 100644 --- a/cmuxTests/BrowserPanelTests.swift +++ b/cmuxTests/BrowserPanelTests.swift @@ -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" ) @@ -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), diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index aca39b2467..92db314ac0 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -1577,6 +1577,68 @@ final class WindowTerminalHostViewTests: XCTestCase { let contentPointInHost = host.convert(contentPointInWindow, from: nil) XCTAssertTrue(host.hitTest(contentPointInHost) === child) } + + func testHostViewStopsSidebarPassThroughJustInsideTerminalContent() { + let terminalSideOverlapWidth: CGFloat = 2 + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 300, height: 180), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + defer { window.orderOut(nil) } + guard let contentView = window.contentView else { + XCTFail("Expected content view") + return + } + + let splitView = NSSplitView(frame: contentView.bounds) + splitView.autoresizingMask = [.width, .height] + splitView.isVertical = true + splitView.dividerStyle = .thin + let splitDelegate = BonsplitMockSplitDelegate() + splitView.delegate = splitDelegate + let first = NSView(frame: NSRect(x: 0, y: 0, width: 120, height: contentView.bounds.height)) + let second = NSView(frame: NSRect(x: 121, y: 0, width: 179, height: contentView.bounds.height)) + splitView.addSubview(first) + splitView.addSubview(second) + contentView.addSubview(splitView) + splitView.setPosition(1, ofDividerAt: 0) + splitView.adjustSubviews() + contentView.layoutSubtreeIfNeeded() + + let host = WindowTerminalHostView(frame: contentView.bounds) + host.autoresizingMask = [.width, .height] + let child = CapturingView(frame: host.bounds) + child.autoresizingMask = [.width, .height] + host.addSubview(child) + contentView.addSubview(host) + + let dividerPointInSplit = NSPoint( + x: splitView.arrangedSubviews[0].frame.maxX + (splitView.dividerThickness * 0.5), + y: splitView.bounds.midY + ) + let dividerPointInWindow = splitView.convert(dividerPointInSplit, to: nil) + let dividerPointInHost = host.convert(dividerPointInWindow, from: nil) + + let resizeBandPoint = NSPoint( + x: dividerPointInHost.x + terminalSideOverlapWidth, + y: dividerPointInHost.y + ) + XCTAssertNil( + host.hitTest(resizeBandPoint), + "The narrow terminal-side overlap should still pass through to the sidebar resizer" + ) + + let textSelectionPoint = NSPoint( + x: dividerPointInHost.x + terminalSideOverlapWidth + 1, + y: dividerPointInHost.y + ) + XCTAssertTrue( + host.hitTest(textSelectionPoint) === child, + "Once the pointer moves past the reduced terminal-side overlap, terminal content should win hit-testing" + ) + } } @@ -1690,6 +1752,102 @@ final class GhosttySurfaceOverlayTests: XCTestCase { XCTAssertTrue(state.isHidden) } + func testPreferredScrollerStyleChangeRecalculatesTerminalSurfaceWidth() { + let surface = TerminalSurface( + tabId: UUID(), + context: GHOSTTY_SURFACE_CONTEXT_SPLIT, + configTemplate: nil, + workingDirectory: nil + ) + let hostedView = surface.hostedView + + let window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 360, height: 240), + styleMask: [.titled, .closable], + backing: .buffered, + defer: false + ) + defer { window.orderOut(nil) } + + guard let contentView = window.contentView else { + XCTFail("Expected content view") + return + } + + hostedView.frame = contentView.bounds + hostedView.autoresizingMask = [.width, .height] + contentView.addSubview(hostedView) + + window.makeKeyAndOrderFront(nil) + window.displayIfNeeded() + contentView.layoutSubtreeIfNeeded() + RunLoop.current.run(until: Date().addingTimeInterval(0.05)) + + guard let scrollView = hostedView.subviews.first(where: { $0 is NSScrollView }) as? NSScrollView else { + XCTFail("Expected hosted terminal scroll view") + return + } + guard let initialSurfaceSize = hostedView.debugPendingSurfaceSize() else { + XCTFail("Expected an initial terminal surface size") + return + } + + let initialContentWidth = scrollView.contentSize.width + XCTAssertEqual(initialSurfaceSize.width, initialContentWidth, accuracy: 0.5) + + scrollView.scrollerStyle = .legacy + scrollView.layoutSubtreeIfNeeded() + let legacyContentWidth = scrollView.contentSize.width + XCTAssertLessThan( + legacyContentWidth, + initialContentWidth, + "Legacy scrollbars should reserve width in the scroll view content area" + ) + XCTAssertEqual( + hostedView.debugPendingSurfaceSize()?.width, + initialSurfaceSize.width, + accuracy: 0.5, + "Changing the scroll view style alone should leave the terminal grid stale until the scroller-style observer runs" + ) + + NotificationCenter.default.post(name: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil) + RunLoop.current.run(until: Date().addingTimeInterval(0.05)) + + XCTAssertEqual(scrollView.scrollerStyle, .legacy) + XCTAssertEqual( + hostedView.debugPendingSurfaceSize()?.width, + legacyContentWidth, + accuracy: 0.5, + "Preferred scroller style changes should recalculate the terminal grid width immediately" + ) + + scrollView.scrollerStyle = .overlay + scrollView.layoutSubtreeIfNeeded() + let overlayContentWidth = scrollView.contentSize.width + XCTAssertGreaterThan( + overlayContentWidth, + legacyContentWidth, + "Overlay scrollbars should restore the full terminal content width" + ) + XCTAssertEqual( + hostedView.debugPendingSurfaceSize()?.width, + legacyContentWidth, + accuracy: 0.5, + "Changing the scroll view style alone should leave the terminal grid stale until the scroller-style observer runs" + ) + + NotificationCenter.default.post(name: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil) + RunLoop.current.run(until: Date().addingTimeInterval(0.05)) + + XCTAssertEqual(scrollView.scrollerStyle, .overlay) + XCTAssertEqual( + hostedView.debugPendingSurfaceSize()?.width, + overlayContentWidth, + accuracy: 0.5, + "Preferred scroller style changes should also restore the wider terminal grid when overlay scrollbars return" + ) + } + func testWindowResignKeyClearsFocusedTerminalFirstResponder() { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 360, height: 240), From 1073aad5200b2ee93c022c81f3ad932e3b659444 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 21 Mar 2026 15:21:33 -0700 Subject: [PATCH 2/4] fix: narrow sidebar overlap and resync terminal width --- Sources/BrowserWindowPortal.swift | 4 ++-- Sources/ContentView.swift | 26 ++++++++++++++++---------- Sources/GhosttyTerminalView.swift | 25 +++++++++++++++++++++++++ Sources/Panels/BrowserPanelView.swift | 2 +- Sources/TerminalWindowPortal.swift | 4 ++-- 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index d3f3b2221c..646e8aba6b 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -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 } diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 2cc83ee299..c20b05a7ec 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -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 } } @@ -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 { @@ -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 } @@ -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 @@ -2055,7 +2061,7 @@ struct ContentView: View { sidebarResizerHandleOverlay( .divider, - width: sidebarResizerHitWidthPerSide * 2, + width: SidebarResizeInteraction.totalHitWidth, availableWidth: totalWidth, accessibilityIdentifier: "SidebarResizer" ) diff --git a/Sources/GhosttyTerminalView.swift b/Sources/GhosttyTerminalView.swift index 66c6b04e4e..90bf075495 100644 --- a/Sources/GhosttyTerminalView.swift +++ b/Sources/GhosttyTerminalView.swift @@ -7005,6 +7005,16 @@ final class GhosttySurfaceScrollView: NSView { 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) { @@ -9040,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 diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index e8d73a3390..adb0fde990 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -5270,7 +5270,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 { diff --git a/Sources/TerminalWindowPortal.swift b/Sources/TerminalWindowPortal.swift index 0518e37cca..4c4588a8a2 100644 --- a/Sources/TerminalWindowPortal.swift +++ b/Sources/TerminalWindowPortal.swift @@ -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 } From 8396885fccdbc104e02f4b2a265b1055563aeb47 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 22 Mar 2026 17:37:07 -0700 Subject: [PATCH 3/4] test: unwrap pending surface width in scrollbar regression --- cmuxTests/TerminalAndGhosttyTests.swift | 37 +++++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index 92db314ac0..ae363849c2 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -1792,6 +1792,27 @@ final class GhosttySurfaceOverlayTests: XCTestCase { return } + func assertPendingSurfaceWidth( + _ expectedWidth: CGFloat, + _ message: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard let pendingSurfaceWidth = hostedView.debugPendingSurfaceSize()?.width else { + XCTFail("Expected a pending terminal surface size", file: file, line: line) + return + } + + XCTAssertEqual( + pendingSurfaceWidth, + expectedWidth, + accuracy: 0.5, + message, + file: file, + line: line + ) + } + let initialContentWidth = scrollView.contentSize.width XCTAssertEqual(initialSurfaceSize.width, initialContentWidth, accuracy: 0.5) @@ -1803,10 +1824,8 @@ final class GhosttySurfaceOverlayTests: XCTestCase { initialContentWidth, "Legacy scrollbars should reserve width in the scroll view content area" ) - XCTAssertEqual( - hostedView.debugPendingSurfaceSize()?.width, + assertPendingSurfaceWidth( initialSurfaceSize.width, - accuracy: 0.5, "Changing the scroll view style alone should leave the terminal grid stale until the scroller-style observer runs" ) @@ -1814,10 +1833,8 @@ final class GhosttySurfaceOverlayTests: XCTestCase { RunLoop.current.run(until: Date().addingTimeInterval(0.05)) XCTAssertEqual(scrollView.scrollerStyle, .legacy) - XCTAssertEqual( - hostedView.debugPendingSurfaceSize()?.width, + assertPendingSurfaceWidth( legacyContentWidth, - accuracy: 0.5, "Preferred scroller style changes should recalculate the terminal grid width immediately" ) @@ -1829,10 +1846,8 @@ final class GhosttySurfaceOverlayTests: XCTestCase { legacyContentWidth, "Overlay scrollbars should restore the full terminal content width" ) - XCTAssertEqual( - hostedView.debugPendingSurfaceSize()?.width, + assertPendingSurfaceWidth( legacyContentWidth, - accuracy: 0.5, "Changing the scroll view style alone should leave the terminal grid stale until the scroller-style observer runs" ) @@ -1840,10 +1855,8 @@ final class GhosttySurfaceOverlayTests: XCTestCase { RunLoop.current.run(until: Date().addingTimeInterval(0.05)) XCTAssertEqual(scrollView.scrollerStyle, .overlay) - XCTAssertEqual( - hostedView.debugPendingSurfaceSize()?.width, + assertPendingSurfaceWidth( overlayContentWidth, - accuracy: 0.5, "Preferred scroller style changes should also restore the wider terminal grid when overlay scrollbars return" ) } From d77ae219715997f2e68e3fb1bc4ecaf3c9a9c53b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 22 Mar 2026 17:49:06 -0700 Subject: [PATCH 4/4] fix: restore hosted inspector divider drag path --- Sources/Panels/BrowserPanelView.swift | 36 ++++++++--------- cmuxTests/TerminalAndGhosttyTests.swift | 51 ++++++++++++++++++++----- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index adb0fde990..ff34d60779 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -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() @@ -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") @@ -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 } let hit = super.hitTest(point) #if DEBUG @@ -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 } @@ -5255,7 +5256,7 @@ struct WebViewRepresentable: NSViewRepresentable { ) ) #endif - layoutHostedInspectorSideDockIfNeeded(reason: "drag.end") + reapplyHostedInspectorDividerToStoredWidthIfNeeded(reason: "drag.end") } super.mouseUp(with: event) } @@ -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) } } @@ -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 } diff --git a/cmuxTests/TerminalAndGhosttyTests.swift b/cmuxTests/TerminalAndGhosttyTests.swift index ae363849c2..2f2a987a7f 100644 --- a/cmuxTests/TerminalAndGhosttyTests.swift +++ b/cmuxTests/TerminalAndGhosttyTests.swift @@ -1510,6 +1510,34 @@ final class WindowTerminalHostViewTests: XCTestCase { private final class BonsplitMockSplitDelegate: NSObject, NSSplitViewDelegate {} + private func makeHostedTerminalView(frame: NSRect) -> GhosttySurfaceScrollView { + let surfaceView = GhosttyNSView(frame: frame) + let hostedView = GhosttySurfaceScrollView(surfaceView: surfaceView) + hostedView.frame = frame + hostedView.autoresizingMask = [.width, .height] + return hostedView + } + + private func assertHitFallsInsideHostedTerminal( + _ hitView: NSView?, + hostedView: GhosttySurfaceScrollView, + message: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + guard let hitView else { + XCTFail(message, file: file, line: line) + return + } + + XCTAssertTrue( + hitView === hostedView || hitView.isDescendant(of: hostedView), + message, + file: file, + line: line + ) + } + func testHostViewPassesThroughWhenNoTerminalSubviewIsHit() { let host = WindowTerminalHostView(frame: NSRect(x: 0, y: 0, width: 200, height: 120)) @@ -1555,9 +1583,8 @@ final class WindowTerminalHostViewTests: XCTestCase { let host = WindowTerminalHostView(frame: contentView.bounds) host.autoresizingMask = [.width, .height] - let child = CapturingView(frame: host.bounds) - child.autoresizingMask = [.width, .height] - host.addSubview(child) + let hostedView = makeHostedTerminalView(frame: host.bounds) + host.addSubview(hostedView) contentView.addSubview(host) let dividerPointInSplit = NSPoint( @@ -1575,7 +1602,11 @@ final class WindowTerminalHostViewTests: XCTestCase { let contentPointInSplit = NSPoint(x: dividerPointInSplit.x + 40, y: splitView.bounds.midY) let contentPointInWindow = splitView.convert(contentPointInSplit, to: nil) let contentPointInHost = host.convert(contentPointInWindow, from: nil) - XCTAssertTrue(host.hitTest(contentPointInHost) === child) + assertHitFallsInsideHostedTerminal( + host.hitTest(contentPointInHost), + hostedView: hostedView, + message: "Terminal content should keep receiving hits after the divider region" + ) } func testHostViewStopsSidebarPassThroughJustInsideTerminalContent() { @@ -1609,9 +1640,8 @@ final class WindowTerminalHostViewTests: XCTestCase { let host = WindowTerminalHostView(frame: contentView.bounds) host.autoresizingMask = [.width, .height] - let child = CapturingView(frame: host.bounds) - child.autoresizingMask = [.width, .height] - host.addSubview(child) + let hostedView = makeHostedTerminalView(frame: host.bounds) + host.addSubview(hostedView) contentView.addSubview(host) let dividerPointInSplit = NSPoint( @@ -1634,9 +1664,10 @@ final class WindowTerminalHostViewTests: XCTestCase { x: dividerPointInHost.x + terminalSideOverlapWidth + 1, y: dividerPointInHost.y ) - XCTAssertTrue( - host.hitTest(textSelectionPoint) === child, - "Once the pointer moves past the reduced terminal-side overlap, terminal content should win hit-testing" + assertHitFallsInsideHostedTerminal( + host.hitTest(textSelectionPoint), + hostedView: hostedView, + message: "Once the pointer moves past the reduced terminal-side overlap, terminal content should win hit-testing" ) } }