@@ -1683,6 +1683,30 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
16831683 }
16841684 }
16851685
1686+ private final class ScrollbarPostingSurfaceView : GhosttyNSView {
1687+ var nextScrollbar : GhosttyScrollbar ?
1688+
1689+ override func scrollWheel( with event: NSEvent ) {
1690+ super. scrollWheel ( with: event)
1691+ guard let nextScrollbar else { return }
1692+ NotificationCenter . default. post (
1693+ name: . ghosttyDidUpdateScrollbar,
1694+ object: self ,
1695+ userInfo: [ GhosttyNotificationKey . scrollbar: nextScrollbar]
1696+ )
1697+ }
1698+ }
1699+
1700+ private func makeScrollbar( total: UInt64 , offset: UInt64 , len: UInt64 ) -> GhosttyScrollbar {
1701+ GhosttyScrollbar (
1702+ c: ghostty_action_scrollbar_s (
1703+ total: total,
1704+ offset: offset,
1705+ len: len
1706+ )
1707+ )
1708+ }
1709+
16861710 private func findEditableTextField( in view: NSView ) -> NSTextField ? {
16871711 if let field = view as? NSTextField , field. isEditable {
16881712 return field
@@ -1768,6 +1792,79 @@ final class GhosttySurfaceOverlayTests: XCTestCase {
17681792 )
17691793 }
17701794
1795+ func testExplicitWheelScrollKeepsScrollbackPinnedAgainstLaterBottomPacket( ) {
1796+ let window = NSWindow (
1797+ contentRect: NSRect ( x: 0 , y: 0 , width: 360 , height: 240 ) ,
1798+ styleMask: [ . titled, . closable] ,
1799+ backing: . buffered,
1800+ defer: false
1801+ )
1802+ defer { window. orderOut ( nil ) }
1803+
1804+ guard let contentView = window. contentView else {
1805+ XCTFail ( " Expected content view " )
1806+ return
1807+ }
1808+
1809+ let surfaceView = ScrollbarPostingSurfaceView ( frame: NSRect ( x: 0 , y: 0 , width: 160 , height: 120 ) )
1810+ surfaceView. cellSize = CGSize ( width: 10 , height: 10 )
1811+ let hostedView = GhosttySurfaceScrollView ( surfaceView: surfaceView)
1812+ hostedView. frame = contentView. bounds
1813+ hostedView. autoresizingMask = [ . width, . height]
1814+ contentView. addSubview ( hostedView)
1815+
1816+ window. makeKeyAndOrderFront ( nil )
1817+ window. displayIfNeeded ( )
1818+ contentView. layoutSubtreeIfNeeded ( )
1819+ hostedView. layoutSubtreeIfNeeded ( )
1820+ RunLoop . current. run ( until: Date ( ) . addingTimeInterval ( 0.05 ) )
1821+
1822+ guard let scrollView = hostedView. subviews. first ( where: { $0 is NSScrollView } ) as? NSScrollView else {
1823+ XCTFail ( " Expected hosted terminal scroll view " )
1824+ return
1825+ }
1826+
1827+ NotificationCenter . default. post (
1828+ name: . ghosttyDidUpdateScrollbar,
1829+ object: surfaceView,
1830+ userInfo: [ GhosttyNotificationKey . scrollbar: makeScrollbar ( total: 100 , offset: 90 , len: 10 ) ]
1831+ )
1832+ RunLoop . current. run ( until: Date ( ) . addingTimeInterval ( 0.01 ) )
1833+ XCTAssertEqual ( scrollView. contentView. bounds. origin. y, 0 , accuracy: 0.01 )
1834+
1835+ surfaceView. nextScrollbar = makeScrollbar ( total: 100 , offset: 40 , len: 10 )
1836+
1837+ guard let cgEvent = CGEvent (
1838+ scrollWheelEvent2Source: nil ,
1839+ units: . pixel,
1840+ wheelCount: 2 ,
1841+ wheel1: 0 ,
1842+ wheel2: - 12 ,
1843+ wheel3: 0
1844+ ) , let scrollEvent = NSEvent ( cgEvent: cgEvent) else {
1845+ XCTFail ( " Expected scroll wheel event " )
1846+ return
1847+ }
1848+
1849+ scrollView. scrollWheel ( with: scrollEvent)
1850+ RunLoop . current. run ( until: Date ( ) . addingTimeInterval ( 0.01 ) )
1851+ XCTAssertEqual ( scrollView. contentView. bounds. origin. y, 500 , accuracy: 0.01 )
1852+
1853+ NotificationCenter . default. post (
1854+ name: . ghosttyDidUpdateScrollbar,
1855+ object: surfaceView,
1856+ userInfo: [ GhosttyNotificationKey . scrollbar: makeScrollbar ( total: 100 , offset: 90 , len: 10 ) ]
1857+ )
1858+ RunLoop . current. run ( until: Date ( ) . addingTimeInterval ( 0.01 ) )
1859+
1860+ XCTAssertEqual (
1861+ scrollView. contentView. bounds. origin. y,
1862+ 500 ,
1863+ accuracy: 0.01 ,
1864+ " A passive bottom packet should not yank the viewport after an explicit wheel scroll into scrollback "
1865+ )
1866+ }
1867+
17711868 func testInactiveOverlayVisibilityTracksRequestedState( ) {
17721869 let hostedView = GhosttySurfaceScrollView (
17731870 surfaceView: GhosttyNSView ( frame: NSRect ( x: 0 , y: 0 , width: 80 , height: 50 ) )
0 commit comments