-
Notifications
You must be signed in to change notification settings - Fork 621
Description
Overview
Add scroll wheel support to SkiaSharp's iOS/iPadOS and Mac Catalyst MAUI views using UIPanGestureRecognizer with allowedScrollTypesMask, normalized 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,TouchesCancelledvia UIGestureRecognizer - Does NOT have any scroll/wheel gesture recognizer
- Has no wheel delta support
Platform Details
iOS / iPadOS (13.4+)
Native API: UIPanGestureRecognizer with allowedScrollTypesMask
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleScroll))
panRecognizer.allowedScrollTypesMask = [.discrete, .continuous]
// Optionally disable touch-based panning:
// panRecognizer.allowedTouchTypes = []
view.addGestureRecognizer(panRecognizer)
@objc func handleScroll(_ gesture: UIPanGestureRecognizer) {
let translation = gesture.translation(in: view)
// translation.y: CGFloat in UIKit points (cumulative since gesture began)
// Must compute incremental delta by tracking previous translation
gesture.setTranslation(.zero, in: view) // reset for incremental deltas
}Scroll Type Distinction
UIScrollType.discrete— mouse scroll wheel (notched)UIScrollType.continuous— trackpad two-finger scroll (smooth)
⚠️ Limitation:allowedScrollTypesMaskis a filter that configures which scroll types the recognizer accepts — it is NOT a per-event classifier. Once configured with[.discrete, .continuous], there is no publicUIPanGestureRecognizerproperty to determine whether the current event came from a discrete mouse wheel or continuous trackpad. This means the normalization formula must work reasonably for both input types. A future enhancement could use heuristics (e.g., velocity magnitude) to distinguish them.
Raw Values
| Device | Type | translation.y per notch | Notes |
|---|---|---|---|
| Mouse wheel | .discrete |
Varies by device/OS | See calibration note below |
| Trackpad | .continuous |
±1-20 points per event | Smooth, fractional |
| Magic Mouse | .continuous |
±1-20 points per event | Same as trackpad |
⚠️ Calibration Note: There is no public iOS API that exposes a "points per notch" constant. TheallowedScrollTypesMaskonly classifies input as.discretevs.continuous— it provides no normalization factor. Developer reports show values ranging from ~22 to ~60 points per notch depending on device, iOS version, and system acceleration settings. This value must be calibrated empirically on real hardware.
Sign Convention
In UIKit, the coordinate origin is at the top-left with Y increasing downward:
translation.y < 0= finger moved toward top of screen (up)translation.y > 0= finger moved toward bottom of screen (down)
Mapping to scroll semantics:
When a user moves their finger up on a trackpad or scrolls a mouse wheel forward, content scrolls upward (revealing content below). In UIKit terms, this is a negative translation.y. In the v120 standard (matching Windows), "scroll up" = positive.
⚠️ Note on terminology: "Scroll up" is ambiguous — it can mean the gesture direction (finger/wheel moves up) or the content direction (content moves up). SkiaSharp follows the Windows convention: positive WheelDelta = wheel rotated forward/away from user = content moves down = user scrolls "up" through content. Therefore: negatetranslation.yto match v120.
Mac Catalyst
Mac Catalyst bridges NSEvent scroll wheel events from AppKit. The same UIPanGestureRecognizer approach works, or you can directly use NSEvent via Catalyst bridging for more precise control:
// Mac Catalyst can also receive NSEvent scroll events
// The UIPanGestureRecognizer approach works for both iOS and CatalystOfficial Documentation
- UIPanGestureRecognizer.allowedScrollTypesMask — enables scroll input from mouse/trackpad (iOS 13.4+)
- UIScrollType —
.discrete(mouse wheel) vs.continuous(trackpad) - UIPanGestureRecognizer — base pan gesture recognizer class
- UIPanGestureRecognizer.translation(in:) — "interprets the pan gesture in the coordinate system of the specified view"
- WWDC 2020: Handle trackpad and mouse input — Apple session on pointer/scroll handling for iPadOS
- Mac Catalyst documentation — general Catalyst overview
- UIKit Coordinate System — origin at upper-left, positive Y extends downward
Normalization Logic
The challenge on iOS is that translation is in UIKit points, not a standardized unit. The scaling factor needs to map "one mouse notch" to 120.
// Approach: Use a calibration constant for discrete mouse input.
// This MUST be validated on real hardware — Apple provides no API for this.
const double PointsPerNotch = 40.0; // Initial estimate — needs empirical testing!
double translationY = gesture.TranslationInView(view).Y;
gesture.SetTranslation(CGPoint.Empty, view); // reset for incremental
// Negate: UIKit negative-Y (finger up) = v120 positive (scroll up)
int wheelDelta = (int)Math.Round(-translationY * 120.0 / PointsPerNotch);
⚠️ Important: ThePointsPerNotchvalue is an initial estimate. It has been reported as anywhere from 22 to 60 depending on the mouse and iOS version. During development, log rawtranslation.yvalues with a known mouse to determine the actual value on your test hardware. Consider making this configurable or using heuristics based on theUIScrollTypeif it becomes available per-event.
Expected Results (calibration-dependent)
| Input | Calculation (assuming 40pt/notch) | WheelDelta |
|---|---|---|
| Mouse notch up (-40pt) | round(-(-40) × 3.0) |
120 |
| Mouse notch down (40pt) | round(-(40) × 3.0) |
-120 |
| Trackpad micro (-2pt) | round(-(-2) × 3.0) |
6 |
| Trackpad large (-15pt) | round(-(-15) × 3.0) |
45 |
Implementation Notes
- Minimum iOS version: 13.4 (when
allowedScrollTypesMaskwas introduced). Guard with@available(iOS 13.4, *)or equivalent MAUI version check. - The Apple
SKTouchHandleris shared between iOS, macOS, and Mac Catalyst — use platform#ifguards or separate the scroll handling - Add a new
UIPanGestureRecognizerspecifically for scroll (separate from touch panning) - Set
allowedTouchTypes = []on the scroll recognizer to prevent it from also capturing finger touches - Fire
SKTouchAction.WheelChangedwithSKTouchDeviceType.Mouse - Use
gesture.Location(in: view)for pointer position - Reset translation with
gesture.SetTranslation(CGPoint.Empty, view)each event to get incremental deltas - Gesture states: Handle
.began,.changed,.ended— fire WheelChanged on.changed - Feed
args.Handledback appropriately - Empirical calibration required: Test with physical Bluetooth mouse + iPad and Magic Keyboard trackpad. Log raw values to determine actual
PointsPerNotchbefore finalizing.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status