Use notifications to deal with NSScrollPocket

pull/9446/head
Daniel Wennberg 2025-11-03 22:48:54 -08:00
parent bbaee5e0a0
commit d678e2e305
1 changed files with 56 additions and 14 deletions

View File

@ -102,6 +102,19 @@ class SurfaceScrollView: NSView {
self?.handleLiveScroll() self?.handleLiveScroll()
}) })
// Listen for frame change events. See the docstring for
// handleFrameChange for why this is necessary.
observers.append(NotificationCenter.default.addObserver(
forName: NSView.frameDidChangeNotification,
object: nil,
// Since this observer is used to immediately override the event
// that produced the notification, we let it run synchronously on
// the posting thread.
queue: nil
) { [weak self] notification in
self?.handleFrameChange(notification)
})
// Listen for derived config changes to update scrollbar settings live // Listen for derived config changes to update scrollbar settings live
surfaceView.$derivedConfig surfaceView.$derivedConfig
.sink { [weak self] _ in .sink { [weak self] _ in
@ -134,20 +147,6 @@ class SurfaceScrollView: NSView {
override func layout() { override func layout() {
super.layout() super.layout()
// The SwiftUI ScrollView host likes to add its own styling overlays to
// the titlebar area, which are incompatible with the hidden titlebar
// style. They won't be present when the app is first opened, but will
// appear when creating splits or cycling fullscreen. There's no public
// way to disable them in AppKit, so we just have to play whack-a-mole.
// See https://developer.apple.com/forums/thread/798392.
if window is HiddenTitlebarTerminalWindow {
for view in scrollView.subviews {
if view.className.contains("NSScrollPocket") {
view.removeFromSuperview()
}
}
}
// Fill entire bounds with scroll view // Fill entire bounds with scroll view
scrollView.frame = bounds scrollView.frame = bounds
@ -283,6 +282,49 @@ class SurfaceScrollView: NSView {
scrollView.reflectScrolledClipView(scrollView.contentView) scrollView.reflectScrolledClipView(scrollView.contentView)
} }
/// Handles a change in the frame of NSScrollPocket styling overlays
///
/// NSScrollView instances are set up with a subview hierarchy which, as far
/// as I can tell, is intended to add a blur effect to any part of a scroll
/// view that lies under the titlebar, presumably to complement a titlebar
/// using liquid glass transparency. This doesn't work correctly with our
/// hidden titlebar style, which does have a titlebar container, albeit
/// hidden. The styling overlays don't care and size themselves to this
/// container, creating a blurry, transparent field that clips the top of
/// the surface view.
///
/// With other titlebar styles, these views always have zero frame size,
/// presumably because there is no overlap between the scroll view and the
/// titlebar container.
///
/// In native fullscreen, the titlebar detaches from the window and these
/// views seem to work a bit differently, taking non-zero sizes for all
/// styles without creating any problems.
///
/// To handle this in a way that minimizes the difference between how the
/// hidden titlebar and other window styles behave, we do as follows: If we
/// have the hidden titlebar style and we're not fullscreen, we listen to
/// frame changes on NSScrollPocket-related objects in scrollView.subviews,
/// and reset their frame to zero.
///
/// See also https://developer.apple.com/forums/thread/798392.
private func handleFrameChange(_ notification: Notification) {
guard let window = window as? HiddenTitlebarTerminalWindow else { return }
guard !window.styleMask.contains(.fullScreen) else { return }
guard let view = notification.object as? NSView else { return }
guard view.className.contains("NSScrollPocket") else { return }
guard scrollView.subviews.contains(view) else { return }
// These guards to avoid an infinite loop don't actually seem necessary.
// The number of times we reach this point during any given event (e.g.,
// creating a split) is the same either way. We keep them anyway out of
// an abundance of caution.
view.postsFrameChangedNotifications = false
view.frame = NSRect(x: 0, y: 0, width: 0, height: 0)
view.postsFrameChangedNotifications = true
}
// MARK: Calculations
/// Calculate the appropriate document view height given a scrollbar state /// Calculate the appropriate document view height given a scrollbar state
private func documentHeight(_ scrollbar: Ghostty.Action.Scrollbar?) -> CGFloat { private func documentHeight(_ scrollbar: Ghostty.Action.Scrollbar?) -> CGFloat {
let contentHeight = scrollView.contentSize.height let contentHeight = scrollView.contentSize.height