feat: scrolling the settings update the sidebar (closes #5400)

This commit is contained in:
lwouis committed 2026-03-09 23:09:46 +01:00
1 parent dbadb2b4e5
commit 3b831d4e59
1 file changed
+43
@@ -477,6 +477,9 @@ class SettingsWindow: NSWindow {
private static let sectionInterSectionSpacing = CGFloat(15)
private static let sectionBottomSpacing = CGFloat(30) - sectionInterSectionSpacing
private static let sectionScrollTopPadding = CGFloat(20)
private static let sectionSelectionTriggerRatioWhenScrollingDown = CGFloat(0.4)
private static let sectionSelectionTriggerRatioWhenScrollingUp = CGFloat(0.6)
private static let sectionSelectionDirectionDeltaThreshold = CGFloat(0.25)
private static let minWindowHeight = CGFloat(500)
private static let sidebarTopInset = CGFloat(40)
private static let sidebarHorizontalPadding = CGFloat(10)
@@ -512,6 +515,8 @@ class SettingsWindow: NSWindow {
private var selectedSectionId: String?
private var sheetHighlightTargets = [ObjectIdentifier: [SettingsSearchHighlightTarget]]()
private var liveResizeOriginX: CGFloat?
private var sectionSelectionTriggerRatio = SettingsWindow.sectionSelectionTriggerRatioWhenScrollingDown
private var lastContentScrollY: CGFloat?
convenience init() {
let windowWidth = Self.sidebarWidth + Self.contentWidth + 3 * Self.contentHorizontalPadding
@@ -593,6 +598,7 @@ class SettingsWindow: NSWindow {
sectionsStack.alignment = .leading
sectionsStack.translatesAutoresizingMaskIntoConstraints = false
sectionsDocumentView.addSubview(sectionsStack)
installContentScrollObserver()
NSLayoutConstraint.activate([
rightScrollView.leadingAnchor.constraint(equalTo: contentContainer.leadingAnchor),
rightScrollView.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
@@ -606,6 +612,12 @@ class SettingsWindow: NSWindow {
])
}
private func installContentScrollObserver() {
rightScrollView.contentView.postsBoundsChangedNotifications = true
lastContentScrollY = rightScrollView.contentView.bounds.minY
NotificationCenter.default.addObserver(self, selector: #selector(contentViewBoundsDidChange), name: NSView.boundsDidChangeNotification, object: rightScrollView.contentView)
}
private func setupSearchField(_ parent: NSView) {
searchField.delegate = self
searchField.placeholderString = NSLocalizedString("Search", comment: "")
@@ -1236,6 +1248,37 @@ class SettingsWindow: NSWindow {
targets.forEach { $0.clear() }
}
@objc private func contentViewBoundsDidChange(_ notification: Notification) {
let currentY = rightScrollView.contentView.bounds.minY
updateSectionSelectionTriggerRatio(currentY)
guard let section = sectionAtCurrentScrollPosition(sectionSelectionTriggerRatio) else { return }
guard selectedSectionId != section.id else { return }
selectSection(section, scroll: false)
}
private func updateSectionSelectionTriggerRatio(_ currentY: CGFloat) {
defer { lastContentScrollY = currentY }
guard let lastContentScrollY else { return }
let deltaY = currentY - lastContentScrollY
if deltaY > Self.sectionSelectionDirectionDeltaThreshold {
sectionSelectionTriggerRatio = Self.sectionSelectionTriggerRatioWhenScrollingDown
return
}
if deltaY < -Self.sectionSelectionDirectionDeltaThreshold {
sectionSelectionTriggerRatio = Self.sectionSelectionTriggerRatioWhenScrollingUp
}
}
private func sectionAtCurrentScrollPosition(_ triggerRatio: CGFloat) -> SettingsSection? {
guard !visibleSections.isEmpty else { return nil }
sectionsDocumentView.layoutSubtreeIfNeeded()
let visibleBounds = rightScrollView.contentView.bounds
let sectionTopY = visibleBounds.minY + visibleBounds.height * triggerRatio
return visibleSections.last {
$0.anchor.convert($0.anchor.bounds, to: sectionsDocumentView).minY <= sectionTopY
} ?? visibleSections[0]
}
private func selectSection(_ section: SettingsSection, scroll: Bool, selectInSidebar: Bool = true) {
selectedSectionId = section.id
if selectInSidebar, let row = visibleSections.firstIndex(where: { $0.id == section.id }), sidebarTableView.selectedRow != row {