How Smooth Scrolling Modifies Trackpad Input in Vorssaint-utils
Vorssaint-utils transforms discrete trackpad wheel events into a continuous, physics-based glide by normalizing deltas through continuousDistance, accumulating them in the Engine class, and emitting frame-by-frame animation steps via advance(elapsed:response:) regardless of display refresh rate.
The open-source utility Vorssaint-utils enhances macOS scrolling by intercepting raw hardware events and applying a custom interpolation algorithm. Unlike standard system scrolling, which emits discrete jumps per trackpad gesture, this implementation converts wheel deltas into smooth, momentum-based motion that remains consistent across 30 Hz, 60 Hz, or 144 Hz displays.
Normalizing Raw Trackpad Events with continuousDistance
Before any physics calculations occur, the system must convert hardware-specific wheel units into standard pixel distances. In Sources/Vorssaint/Services/SmoothScrollSupport.swift, the helper function continuousDistance (lines 9‑17) performs this normalization using the user-defined step setting.
This conversion ensures that a trackpad flick produces the same scroll distance regardless of whether the hardware reports in lines, pixels, or arbitrary fixed-point values. The method signature accepts fixedPointDelta, pointDelta, and the configurable step parameter to produce a hardware-agnostic output.
let normalized = SmoothScrollSupport.continuousDistance(
fixedPointDelta: event.fixedPointDelta,
pointDelta: event.pointDelta,
step: Double(smoothScrollStep)
)
Accumulating Scroll Deltas in the Physics Engine
Normalized distances feed into the Engine class via the add(vertical:horizontal:) method. This engine maintains per-axis “budgets” tracked as remainingVertical and remainingHorizontal properties that represent the total distance yet to be scrolled.
When a new gesture arrives, the engine checks if the new direction opposes the current momentum using the directionsOppose logic. If the user reverses direction, the old scroll tail is immediately discarded so the first opposite tick registers without delay.
var scrollEngine = SmoothScrollSupport.Engine()
scrollEngine.add(vertical: normalized, horizontal: 0)
Frame-Independent Animation with advance(elapsed:response:)
The smooth scrolling animation runs on a display-link callback (approximately 60 Hz by default). On each frame, the engine’s advance(elapsed:response:) method calculates how much of the remaining budget to emit.
Inside frameDelta (lines 71‑82), the code applies an exponential-decay curve whose time constant derives from the response setting. Crucially, the calculation uses the actual elapsed time passed into advance, making the scroll shape identical on 30 Hz, 60 Hz, or 144 Hz displays.
The computed delta is then split into wholePixels and a fractional carry (lines 19‑28) to prevent cumulative rounding errors. On the final frame, finalPixels flushes any remaining fraction to ensure the view lands exactly at the target position.
let frame = scrollEngine.advance(
elapsed: displayLink.timestamp - lastTimestamp,
response: smoothScrollResponse
)
applyScroll(deltaX: frame.horizontal, deltaY: frame.vertical)
Handling Direction Reversals and Shift-Modified Scrolling
The engine respects immediate user intent during gesture reversals. When directionsOppose detects a direction change, it discards the existing momentum buffer so the scroll responds instantly rather than fighting the new input.
For accessibility, Vorssaint-utils implements axis swapping in axes(vertical:horizontal:shiftPressed:) (lines 51‑63). When the user holds the Shift key, vertical wheel ticks redirect to horizontal scrolling while preserving the original sign and magnitude.
Configuring Smooth Scrolling in SettingsView.swift
User preferences bind directly to the engine’s behavior through Sources/Vorssaint/UI/Settings/SettingsView.swift (lines 906‑1022). Three @AppStorage properties control the feature:
smoothScrollEnabled– Toggles the interpolation on or offsmoothScrollStep– Adjusts the distance multiplier applied during normalizationsmoothScrollResponse– Controls the exponential decay rate (higher values feel snappier)
Changes to these values update the engine’s parameters in real-time without requiring an app restart.
@AppStorage(DefaultsKey.smoothScrollEnabled) private var smoothScrollEnabled = false
@AppStorage(DefaultsKey.smoothScrollStep) private var smoothScrollStep = SmoothScrollSupport.defaultStep
@AppStorage(DefaultsKey.smoothScrollResponse) private var smoothScrollResponse = SmoothScrollSupport.defaultResponse
Summary
- Normalization:
continuousDistanceconverts hardware-specific trackpad units to pixels using the configurablestepvalue. - Accumulation: The
Engineclass adds deltas to per-axis budgets and discards stale momentum whendirectionsOpposedetects a reversal. - Frame Independence:
advance(elapsed:response:)uses elapsed time calculations to ensure consistent animation curves across all display refresh rates. - Precision: Rounding logic splits deltas into
wholePixelsand fractional carries, flushing remainders viafinalPixelson the last frame. - Accessibility:
axes(vertical:horizontal:shiftPressed:)redirects vertical input to horizontal scrolling when Shift is held. - Configuration:
SettingsView.swiftbinds UI controls toDefaultsKeystorage for instant updates.
Frequently Asked Questions
What file contains the core smooth scrolling logic in Vorssaint-utils?
The physics engine lives in Sources/Vorssaint/Services/SmoothScrollSupport.swift, which defines the Engine class, continuousDistance normalization, and advance(elapsed:response:) animation logic. The public façade that connects this engine to the app’s event loop resides in Sources/Vorssaint/Services/SmoothScrollService.swift.
How does Vorssaint-utils ensure smooth scrolling feels the same on 144Hz and 60Hz monitors?
The advance(elapsed:response:) method accepts an elapsed time parameter rather than assuming a fixed frame duration. The frameDelta calculation (lines 71‑82) uses this elapsed value to apply the exponential decay curve, ensuring the same gesture produces identical scroll shapes regardless of whether the display refreshes at 30 Hz, 60 Hz, or 144 Hz.
What happens when I reverse scroll direction during a smooth scroll gesture?
The engine checks if new input directionsOppose the current momentum. If so, it immediately discards the remaining scroll budget (remainingVertical or remainingHorizontal) so the trackpad responds to the reverse gesture without fighting the previous momentum’s tail.
How can I disable smooth scrolling or adjust its speed?
Open the settings UI defined in Sources/Vorssaint/UI/Settings/SettingsView.swift. The toggle bound to smoothScrollEnabled disables the feature entirely, while sliders for smoothScrollStep (distance per tick) and smoothScrollResponse (decay speed) adjust the physics in real-time via AppStorage bindings to DefaultsKey values.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →