-
Notifications
You must be signed in to change notification settings - Fork 616
Description
Overview
Add scroll wheel support to SkiaSharp's macOS MAUI views by overriding scrollWheel(with:) in the Apple SKTouchHandler and normalizing NSEvent.scrollingDeltaY values to the v120 standard (120 = one discrete mouse wheel notch).
Parent issue: #3533
Current State
The Apple SKTouchHandler (source/SkiaSharp.Views.Maui/SkiaSharp.Views.Maui.Core/Platform/Apple/SKTouchHandler.cs) currently:
- Handles:
TouchesBegan,TouchesMoved,TouchesEnded,TouchesCancelled - Does NOT override:
scrollWheel(with:) - Has no wheel delta support whatsoever
Platform Details
Native API
NSEvent scroll wheel properties:
override func scrollWheel(with event: NSEvent) {
let deltaY = event.scrollingDeltaY
let isPrecise = event.hasPreciseScrollingDeltas
let isInverted = event.isDirectionInvertedFromDevice
// deltaY: CGFloat
// isPrecise: Bool — true for trackpad/Magic Mouse, false for mouse wheel
// isInverted: Bool — true when natural scrolling is enabled
}Raw Values Per Discrete Mouse Notch
| Device | hasPreciseScrollingDeltas | scrollingDeltaY | Notes |
|---|---|---|---|
| Standard mouse wheel | false |
±1.0 per notch | "Line" units |
| Fast mouse scroll | false |
±2.0, ±3.0 | System acceleration |
| Trackpad | true |
Fractional, variable | "Pixel-like" units — NOT line units |
| Magic Mouse | true |
Fractional, variable | Same as trackpad |
⚠️ Important: Apple does not publish official numeric ranges for trackpadscrollingDeltaY. Values are device-dependent and vary with gesture speed. Do not hardcode scaling assumptions — usehasPreciseScrollingDeltasto branch logic.
Sign Convention & Natural Scrolling
scrollingDeltaY > 0= scroll up (content moves down) — when natural scrolling is OFFscrollingDeltaY < 0= scroll down (content moves up) — when natural scrolling is OFF
⚠️ CRITICAL:scrollingDeltaYdoes NOT always reflect the physical gesture direction. When the user has "Natural scrolling" enabled in System Preferences, AppKit automatically inverts the delta values before delivering them to your app. Apple's documentation states:"The user may choose to change the scrolling behavior such that it feels like they are moving the content instead of the scroll bar. To accomplish this, deltaX and deltaY and scrollingDeltaX and scrollingDeltaY values are automatically inverted for NSEventScrollWheel events according to the user's preferences."
— NSEvent.isDirectionInvertedFromDeviceUse
isDirectionInvertedFromDeviceto detect this. If you need the physical hardware direction (for v120 normalization), compensate by negating whenisDirectionInvertedFromDevice == true.
Distinguishing Mouse vs Trackpad (REQUIRED)
Using the same scaling formula for both mouse and trackpad will produce wildly incorrect results. Apple's documentation states:
"When hasPreciseScrollingDeltas is false, multiply the value returned by scrollingDeltaY by the line or row height. Otherwise scroll by the returned amount."
— NSEvent.scrollingDeltaY
hasPreciseScrollingDeltas == false→ discrete mouse wheel (line units: ~1.0 per notch). Multiply by 120 to get v120.hasPreciseScrollingDeltas == true→ continuous trackpad or Magic Mouse (pixel-like units). These are already in a fine-grained unit — do NOT multiply by 120 or you'll get extreme values.
Official Documentation
- NSEvent.scrollingDeltaY — "the scroll wheel delta in the vertical direction"
- NSEvent.scrollingDeltaX — horizontal equivalent
- NSEvent.hasPreciseScrollingDeltas — "whether precise scrolling deltas are available" (trackpad vs mouse)
- NSEvent.isDirectionInvertedFromDevice — whether natural scrolling has inverted the reported deltas
- NSEvent class reference — full event class
- NSEvent.deltaY — older coarser property (prefer scrollingDeltaY)
Normalization Logic
// In scrollWheel(with:) handler:
double deltaY = nsEvent.ScrollingDeltaY;
// Step 1: Undo natural scrolling inversion to get physical direction
if (nsEvent.IsDirectionInvertedFromDevice)
deltaY = -deltaY;
// Step 2: Scale based on device type
int wheelDelta;
if (!nsEvent.HasPreciseScrollingDeltas)
{
// Mouse wheel: line units (~1.0 per notch). Direct ×120.
wheelDelta = (int)Math.Round(deltaY * 120.0);
}
else
{
// Trackpad/Magic Mouse: pixel-like units.
// These are already fine-grained. Scale proportionally.
// Calibration: trackpad values are roughly in "points scrolled"
// and don't map 1:1 to mouse notches. Pass as sub-notch v120.
wheelDelta = (int)Math.Round(deltaY * 120.0 / 10.0);
// A 10-point trackpad swipe → 120 (one notch equivalent)
// A 1-point micro gesture → 12 (sub-notch)
}
⚠️ Note: The trackpad scaling factor (/ 10.0) is an initial estimate. The exact relationship between trackpad points and mouse notches is not defined by Apple and may vary by device. This should be validated on hardware and may need tuning.
Expected Results
| Input | hasPrecise | isInverted | Calculation | WheelDelta |
|---|---|---|---|---|
| Mouse notch up (1.0) | false | false | round(1.0 × 120) |
120 |
| Mouse notch up (1.0), natural scrolling ON | false | true | round(-1.0 × -1 × 120) |
120 |
| Mouse notch down (-1.0) | false | false | round(-1.0 × 120) |
-120 |
| Fast mouse (3.0) | false | false | round(3.0 × 120) |
360 |
| Trackpad 10pt swipe up | true | false | round(10.0 × 12) |
120 |
| Trackpad 1pt micro | true | false | round(1.0 × 12) |
12 |
Implementation Notes
- Need to add
scrollWheel(with:)override to the macOS view or gesture recognizer setup - The Apple
SKTouchHandleris shared between iOS, macOS, and Mac Catalyst — may need platform-specific#iffor macOS scroll wheel - Fire
SKTouchAction.WheelChangedwithSKTouchDeviceType.Mouse - Get pointer position from
nsEvent.LocationInWindowconverted to view coordinates - Set
inContact = false - Feed
args.Handledback to the event system - Reference: Windows handler in
Platform/Windows/SKTouchHandler.csfor the pattern - Consider also handling
scrollingDeltaXfor horizontal scroll (future enhancement) - Always handle
isDirectionInvertedFromDevice— most Mac users have natural scrolling enabled
Metadata
Metadata
Assignees
Labels
Type
Projects
Status