Skip to content

feat(ui): use thin overlay scrollers app-wide#41

Merged
BlackHole1 merged 1 commit into
mainfrom
feat/overlay-scrollers
Jun 29, 2026
Merged

feat(ui): use thin overlay scrollers app-wide#41
BlackHole1 merged 1 commit into
mainfrom
feat/overlay-scrollers

Conversation

@BlackHole1

Copy link
Copy Markdown
Member

With the system Appearance ▸ Show scroll bars preference set to "Always", every scroll surface in the app drew macOS's wide, always-visible legacy scrollers — a thick gutter pinned open even when nothing was scrolling. SwiftUI only lets you control scroll-indicator visibility (.scrollIndicators), not scroller style (which lives on NSScrollView.scrollerStyle), so the system preference leaks into the app with no SwiftUI escape hatch.

This adds an .overlayScrollers() modifier that drops a zero-size, event-transparent probe into the view's background, finds the host NSWindow, and sweeps every NSScrollView to .overlay — the thin bars that appear on scroll and fade out afterward. Overlay scrollers also float over the content instead of reserving a gutter, so this reclaims the width the legacy bars ate. The sweep re-asserts overlay on preferredScrollerStyleDidChangeNotification (AppKit resets the style whenever the user flips the system preference) and runs again on the next runloop turn to catch scroll views SwiftUI mounts lazily.

It's applied to every scrolling surface: the Settings window (keyed on the selected tab so each lazily-mounted pane and the Log table is caught), the update window, the app picker, the acknowledgements list, and the import review sheet.

Verified visually with the system set to "Always": the thick persistent bar is gone and a thin overlay bar appears only while scrolling.

When the system *Appearance ▸ Show scroll bars* setting is "Always",
every scroll surface drew the wide, always-visible legacy scrollers —
a thick gutter pinned open even when nothing was scrolling. SwiftUI
only exposes scroll-indicator *visibility* (`.scrollIndicators`), not
scroller *style*, which lives on `NSScrollView.scrollerStyle`, so the
preference leaks into the app with no SwiftUI escape hatch.

Add an `.overlayScrollers()` modifier that drops a zero-size,
event-transparent probe into the background, finds the host
`NSWindow`, and sweeps every `NSScrollView` to `.overlay` — the thin
bars that appear on scroll and fade out afterward, and float over the
content instead of reserving a gutter, reclaiming the width the legacy
bars ate. The sweep re-asserts overlay on
`preferredScrollerStyleDidChangeNotification` (AppKit resets the style
when the user flips the system preference) and runs again on the next
runloop turn to catch scroll views SwiftUI mounts lazily.

Apply it to every scrolling surface: the Settings window (keyed on the
selected tab so each lazily-mounted pane and the Log table is caught),
the update window, the app picker, the acknowledgements list, and the
import review sheet.

Signed-off-by: Kevin Cui <bh@bugs.cc>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features
    • Added overlay-style scrollbars throughout the app’s macOS sheets and settings views.
    • Scroll indicators now automatically match the system “Show scroll bars” preference and refresh when views change.
  • UI Improvements
    • Applied the new scroll behavior to acknowledgements, app selection, import review, settings, and update screens for a more consistent look and feel.

Walkthrough

A new OverlayScrollers.swift file adds a SwiftUI View.overlayScrollers(trigger:) modifier backed by an NSViewRepresentable probe (OverlayScrollerSweep) that installs a zero-size, hit-test-transparent SweepProbe (NSView). The probe recursively sets scrollerStyle to .overlay on all NSScrollViews in the window, re-applies on NSScroller.preferredScrollerStyleDidChangeNotification, and schedules a deferred second pass. This modifier is then applied to SettingsRootView, AcknowledgementsView, AppPickerSheet, ImportReviewSheet, and UpdateWindowView.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required type(scope): subject format and accurately describes the overlay scroller change.
Description check ✅ Passed The description clearly matches the app-wide overlay scroller implementation and its affected screens.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/overlay-scrollers

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
Sources/LockIME/Support/OverlayScrollers.swift (1)

26-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Gate full-window sweeps on trigger changes.

updateNSView rescans the whole window on every SwiftUI invalidation, so views like AppPickerSheet and UpdateWindowView will do two tree walks on each search or progress update. The API already exposes trigger; use it to re-sweep only when the mounted scroll-view set may have changed.

Proposed change
 private struct OverlayScrollerSweep: NSViewRepresentable {
     let trigger: AnyHashable

     func makeNSView(context: Context) -> SweepProbe { SweepProbe(frame: .zero) }
-    func updateNSView(_ probe: SweepProbe, context: Context) { probe.sweep() }
+    func updateNSView(_ probe: SweepProbe, context: Context) {
+        probe.update(trigger: trigger)
+    }
 }
 
 /// Zero-size, event-transparent probe that re-styles its window's scroll views.
 private final class SweepProbe: NSView {
+    private var lastTrigger: AnyHashable?
+
+    func update(trigger: AnyHashable) {
+        guard lastTrigger != trigger else { return }
+        lastTrigger = trigger
+        sweep()
+    }
+
     // The probe sits behind the content, but never intercept events meant for it.
     override func hitTest(_ point: NSPoint) -> NSView? { nil }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/LockIME/Support/OverlayScrollers.swift` around lines 26 - 30, The
`OverlayScrollerSweep` representable is sweeping the entire window on every
SwiftUI update, even when `trigger` has not changed. Update
`updateNSView(_:context:)` to cache the last seen `trigger` in the
`SweepProbe`/context and call `sweep()` only when that value changes, so
`AppPickerSheet` and `UpdateWindowView` don’t perform redundant tree walks on
unrelated invalidations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@Sources/LockIME/Support/OverlayScrollers.swift`:
- Around line 26-30: The `OverlayScrollerSweep` representable is sweeping the
entire window on every SwiftUI update, even when `trigger` has not changed.
Update `updateNSView(_:context:)` to cache the last seen `trigger` in the
`SweepProbe`/context and call `sweep()` only when that value changes, so
`AppPickerSheet` and `UpdateWindowView` don’t perform redundant tree walks on
unrelated invalidations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2df883e9-3da1-42ba-a9a4-420dbf575940

📥 Commits

Reviewing files that changed from the base of the PR and between d4879ce and 43817da.

📒 Files selected for processing (6)
  • Sources/LockIME/Support/OverlayScrollers.swift
  • Sources/LockIME/UI/AboutView.swift
  • Sources/LockIME/UI/Settings/AppPickerSheet.swift
  • Sources/LockIME/UI/Settings/ImportReviewSheet.swift
  • Sources/LockIME/UI/SettingsRootView.swift
  • Sources/LockIME/UI/UpdateWindowView.swift

@BlackHole1 BlackHole1 merged commit 60c6938 into main Jun 29, 2026
3 checks passed
@BlackHole1 BlackHole1 deleted the feat/overlay-scrollers branch June 29, 2026 05:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant