macos: always use overlay scroller (#9865)

With this PR, the macos scrollbar always uses the overlay style. If the
OS preferred style is `.legacy`, we flash the scroller when the mouse is
moved over it, such that users can still click and drag without relying
on scroll wheels or gestures.

Implements #9610.

There are a few lines of code that could technically be removed after
this change as they're only needed to make surfaces work correctly with
the legacy scrollbar, but I decided to leave them in since they do no
harm (see code comments). This ensures correct behavior if, for whatever
reason, some corner case brings back the legacy scrollbar, or if someone
decides to experiment with scrollbar styles in the future.
pull/9831/head
Mitchell Hashimoto 2025-12-10 13:55:49 -08:00 committed by GitHub
commit 4a173052fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 40 additions and 3 deletions

View File

@ -34,10 +34,15 @@ class SurfaceScrollView: NSView {
scrollView.hasHorizontalScroller = false
scrollView.autohidesScrollers = false
scrollView.usesPredominantAxisScrolling = true
// Always use the overlay style. See mouseMoved for how we make
// it usable without a scroll wheel or gestures.
scrollView.scrollerStyle = .overlay
// hide default background to show blur effect properly
scrollView.drawsBackground = false
// don't let the content view clip it's subviews, to enable the
// don't let the content view clip its subviews, to enable the
// surface to draw the background behind non-overlay scrollers
// (we currently only use overlay scrollers, but might as well
// configure the views correctly in case we change our mind)
scrollView.contentView.clipsToBounds = false
// The document view is what the scrollview is actually going
@ -107,7 +112,10 @@ class SurfaceScrollView: NSView {
observers.append(NotificationCenter.default.addObserver(
forName: NSScroller.preferredScrollerStyleDidChangeNotification,
object: nil,
queue: .main
// 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] _ in
self?.handleScrollerStyleChange()
})
@ -176,10 +184,10 @@ class SurfaceScrollView: NSView {
private func synchronizeAppearance() {
let scrollbarConfig = surfaceView.derivedConfig.scrollbar
scrollView.hasVerticalScroller = scrollbarConfig != .never
scrollView.verticalScroller?.controlSize = .small
let hasLightBackground = OSColor(surfaceView.derivedConfig.backgroundColor).isLightColor
// Make sure the scrollers appearance matches the surface's background color.
scrollView.appearance = NSAppearance(named: hasLightBackground ? .aqua : .darkAqua)
updateTrackingAreas()
}
/// Positions the surface view to fill the currently visible rectangle.
@ -240,6 +248,7 @@ class SurfaceScrollView: NSView {
/// Handles scrollbar style changes
private func handleScrollerStyleChange() {
scrollView.scrollerStyle = .overlay
synchronizeCoreSurface()
}
@ -350,4 +359,32 @@ class SurfaceScrollView: NSView {
}
return contentHeight
}
// MARK: Mouse events
override func mouseMoved(with: NSEvent) {
// When the OS preferred style is .legacy, the user should be able to
// click and drag the scroller without using scroll wheels or gestures,
// so we flash it when the mouse is moved over the scrollbar area.
guard NSScroller.preferredScrollerStyle == .legacy else { return }
scrollView.flashScrollers()
}
override func updateTrackingAreas() {
// To update our tracking area we just recreate it all.
trackingAreas.forEach { removeTrackingArea($0) }
super.updateTrackingAreas()
// Our tracking area is the scroller frame
guard let scroller = scrollView.verticalScroller else { return }
addTrackingArea(NSTrackingArea(
rect: convert(scroller.bounds, from: scroller),
options: [
.mouseMoved,
.activeInKeyWindow,
],
owner: self,
userInfo: nil))
}
}