Preserve explicit wheel scrollback against passive follow

This commit is contained in:
Lawrence Chen 2026-03-22 18:11:06 -07:00
parent 7634abe616
commit c1c028e628
No known key found for this signature in database

View file

@ -6052,6 +6052,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
}
override func scrollWheel(with event: NSEvent) {
NotificationCenter.default.post(name: .ghosttyDidReceiveWheelScroll, object: self)
guard let surface = surface else { return }
lastScrollEventTime = CACurrentMediaTime()
Self.focusLog("scrollWheel: surface=\(terminalSurface?.id.uuidString ?? "nil") firstResponder=\(String(describing: window?.firstResponder))")
@ -6453,6 +6454,7 @@ enum GhosttyNotificationKey {
extension Notification.Name {
static let ghosttyDidUpdateScrollbar = Notification.Name("ghosttyDidUpdateScrollbar")
static let ghosttyDidUpdateCellSize = Notification.Name("ghosttyDidUpdateCellSize")
static let ghosttyDidReceiveWheelScroll = Notification.Name("ghosttyDidReceiveWheelScroll")
static let ghosttySearchFocus = Notification.Name("ghosttySearchFocus")
static let ghosttyConfigDidReload = Notification.Name("ghosttyConfigDidReload")
static let ghosttyDefaultBackgroundDidChange = Notification.Name("ghosttyDefaultBackgroundDidChange")
@ -6586,6 +6588,8 @@ final class GhosttySurfaceScrollView: NSView {
/// When true, auto-scroll should be suspended to prevent the "doomscroll" bug
/// where the terminal fights the user's scroll position.
private var userScrolledAwayFromBottom = false
private var pendingExplicitWheelScroll = false
private var allowExplicitScrollbarSync = false
/// Threshold in points from bottom to consider "at bottom" (allows for minor float drift)
private static let scrollToBottomThreshold: CGFloat = 5.0
private var isActive = true
@ -7011,6 +7015,14 @@ final class GhosttySurfaceScrollView: NSView {
self?.handleScrollbarUpdate(notification)
})
observers.append(NotificationCenter.default.addObserver(
forName: .ghosttyDidReceiveWheelScroll,
object: surfaceView,
queue: .main
) { [weak self] _ in
self?.pendingExplicitWheelScroll = true
})
observers.append(NotificationCenter.default.addObserver(
forName: .ghosttySearchFocus,
object: nil,
@ -9013,19 +9025,12 @@ final class GhosttySurfaceScrollView: NSView {
userScrolledAwayFromBottom = false
}
// Only auto-scroll if user hasn't manually scrolled away from bottom
// or if we're following terminal output (scrollbar shows we're at bottom)
let shouldAutoScroll = !userScrolledAwayFromBottom ||
(scrollbar.offset + scrollbar.len >= scrollbar.total)
// Passive bottom packets should not override an explicit scrollback review,
// but the first scrollbar packet caused by the user's own wheel input should
// still move the viewport to the requested scrollback position.
let shouldAutoScroll = !userScrolledAwayFromBottom || allowExplicitScrollbarSync
if shouldAutoScroll && !pointApproximatelyEqual(currentOrigin, targetOrigin) {
#if DEBUG
logDragGeometryChange(
event: "scrollOrigin",
old: currentOrigin,
new: targetOrigin
)
#endif
scrollView.contentView.scroll(to: targetOrigin)
didChangeGeometry = true
}
@ -9033,6 +9038,8 @@ final class GhosttySurfaceScrollView: NSView {
}
}
allowExplicitScrollbarSync = false
if didChangeGeometry {
scrollView.reflectScrolledClipView(scrollView.contentView)
}
@ -9068,6 +9075,11 @@ final class GhosttySurfaceScrollView: NSView {
guard let scrollbar = notification.userInfo?[GhosttyNotificationKey.scrollbar] as? GhosttyScrollbar else {
return
}
if pendingExplicitWheelScroll {
userScrolledAwayFromBottom = scrollbar.offset + scrollbar.len < scrollbar.total
allowExplicitScrollbarSync = true
pendingExplicitWheelScroll = false
}
surfaceView.scrollbar = scrollbar
synchronizeScrollView()
}