-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Preserve explicit wheel scrollback against passive follow #1965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 } | ||||||||||||
|
Comment on lines
+6055
to
6056
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The notification fires even when Moving the post to just after the guard ensures the flag is only set when there is an actual surface to drive the corresponding scrollbar update:
Suggested change
|
||||||||||||
| 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 | ||||||||||||
| }) | ||||||||||||
|
Comment on lines
+7022
to
+7024
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||||||||||||
|
|
||||||||||||
| observers.append(NotificationCenter.default.addObserver( | ||||||||||||
| forName: .ghosttySearchFocus, | ||||||||||||
| object: nil, | ||||||||||||
|
|
@@ -9013,26 +9025,21 @@ 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 | ||||||||||||
| } | ||||||||||||
| lastSentRow = Int(scrollbar.offset) | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| 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() | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make wheel intent a one-shot token, not a sticky Bool.
pendingExplicitWheelScrollis armed on every wheel event here, but it is only cleared when a scrollbar packet arrives. If that wheel cannot produce a packet — for example Line 6056 returns early becausesurfaceis nil, or the user is already at the scrollback boundary — the next unrelated bottom packet is still treated as explicit input and follow gets re-enabled again. A singleBoolalso collapses multiple wheel events into one pending sync. Please switch this to a per-wheel token/counter with a bounded lifetime, and only arm it after the surface guard succeeds.Also applies to: 6591-6592, 7018-7024, 9078-9082
🤖 Prompt for AI Agents