Fix terminal pane trackpad scroll routing

This commit is contained in:
austinpower1258 2026-02-26 14:43:13 -08:00
parent b0bf4ef514
commit eeb6122e3c
2 changed files with 79 additions and 10 deletions

View file

@ -3700,23 +3700,23 @@ extension Notification.Name {
private final class GhosttyScrollView: NSScrollView {
weak var surfaceView: GhosttyNSView?
// Keep keyboard routing on the terminal surface; this wrapper is viewport plumbing.
override var acceptsFirstResponder: Bool { false }
override func scrollWheel(with event: NSEvent) {
guard let surfaceView else {
super.scrollWheel(with: event)
return
}
if let surface = surfaceView.terminalSurface?.surface,
ghostty_surface_mouse_captured(surface) {
GhosttyNSView.focusLog("GhosttyScrollView.scrollWheel: mouseCaptured -> surface scroll")
if window?.firstResponder !== surfaceView {
window?.makeFirstResponder(surfaceView)
}
surfaceView.scrollWheel(with: event)
} else {
GhosttyNSView.focusLog("GhosttyScrollView.scrollWheel: super scroll")
super.scrollWheel(with: event)
// Route wheel gestures to the terminal surface so Ghostty handles scrollback.
// Letting NSScrollView consume these events moves the wrapper viewport itself,
// which causes pane-content drift instead of terminal scrollback movement.
GhosttyNSView.focusLog("GhosttyScrollView.scrollWheel: surface scroll")
if window?.firstResponder !== surfaceView {
window?.makeFirstResponder(surfaceView)
}
surfaceView.scrollWheel(with: event)
}
}

View file

@ -6571,6 +6571,75 @@ final class CommandPaletteOverlayPromotionPolicyTests: XCTestCase {
@MainActor
final class GhosttySurfaceOverlayTests: XCTestCase {
private final class ScrollProbeSurfaceView: GhosttyNSView {
private(set) var scrollWheelCallCount = 0
override func scrollWheel(with event: NSEvent) {
scrollWheelCallCount += 1
}
}
func testTrackpadScrollRoutesToTerminalSurfaceAndPreservesKeyboardFocusPath() {
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
}
let surfaceView = ScrollProbeSurfaceView(frame: NSRect(x: 0, y: 0, width: 160, height: 120))
let hostedView = GhosttySurfaceScrollView(surfaceView: surfaceView)
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
}
XCTAssertFalse(
scrollView.acceptsFirstResponder,
"Host scroll view should not become first responder and steal terminal shortcuts"
)
_ = window.makeFirstResponder(nil)
guard let cgEvent = CGEvent(
scrollWheelEvent2Source: nil,
units: .pixel,
wheelCount: 2,
wheel1: 0,
wheel2: -12,
wheel3: 0
), let scrollEvent = NSEvent(cgEvent: cgEvent) else {
XCTFail("Expected scroll wheel event")
return
}
scrollView.scrollWheel(with: scrollEvent)
XCTAssertEqual(
surfaceView.scrollWheelCallCount,
1,
"Trackpad wheel events should be forwarded directly to Ghostty surface scrolling"
)
XCTAssertTrue(
window.firstResponder === surfaceView,
"Scroll wheel handling should keep keyboard focus on terminal surface"
)
}
func testInactiveOverlayVisibilityTracksRequestedState() {
let hostedView = GhosttySurfaceScrollView(
surfaceView: GhosttyNSView(frame: NSRect(x: 0, y: 0, width: 80, height: 50))