-
Notifications
You must be signed in to change notification settings - Fork 616
Description
Overview
Add scroll wheel support to SkiaSharp's GTK3/GTK4 views by handling scroll events and normalizing values to the v120 standard (120 = one discrete mouse wheel notch).
Parent issue: #3533
Current State
- GTK3
SKDrawingArea(source/SkiaSharp.Views/SkiaSharp.Views.Gtk3/) — extendsGtk.DrawingArea, only handlesPaintSurface, no input events - GTK4
SKDrawingArea(source/SkiaSharp.Views/SkiaSharp.Views.Gtk4/) — extendsGtk.DrawingAreausing GirCore.Gtk-4.0 (v0.7.0), renders viaSetDrawFunccallback. HandlesPaintSurfaceonly — no scroll or input events yet. Merged in Add SkiaSharp.Views.Gtk4 using GirCore.Gtk-4.0 bindings #3527. - Neither has any
SKTouchHandleror touch event infrastructure
Note: The GTK4 view now exists and is the preferred target for adding scroll support. It uses GirCore bindings which provide access to
Gtk.EventControllerScrollvia theGirCore.Gtk-4.0NuGet package. Linux is unique in that the underlyinglibinputlibrary already uses the v120 convention natively.
Platform Details — GTK4
Native API
Gtk.EventControllerScroll via GirCore:
// C# using GirCore.Gtk-4.0 (matching the existing SKDrawingArea in SkiaSharp.Views.Gtk4)
// Add to SKDrawingArea constructor or initialization:
var scrollController = Gtk.EventControllerScroll.New(
Gtk.EventControllerScrollFlags.Vertical |
Gtk.EventControllerScrollFlags.BothDirections
);
scrollController.OnScroll += (controller, args) =>
{
// args.Dx, args.Dy: double
// Discrete mouse: dy = ±1.0 per notch
// Smooth trackpad: fractional values
int wheelDelta = (int)Math.Round(-args.Dy * 120.0);
// Fire SKTouchAction.WheelChanged with wheelDelta
return true; // handled
};
this.AddController(scrollController);Note: The existing
SKDrawingAreainsource/SkiaSharp.Views/SkiaSharp.Views.Gtk4/usesSkiaSharp.Views.Gtknamespace withGirCore.Gtk-4.0v0.7.0. TheGtk.EventControllerScrollclass is available through this same package — no additional dependencies needed.
Raw Values
| Device | Signal | dy value | Notes |
|---|---|---|---|
| Discrete mouse wheel | scroll |
±1.0 per notch | Exact multiples of 1.0 |
| Smooth trackpad | scroll (with scroll-begin/scroll-end) |
±0.06 to ±3.0 | Arbitrary floating point |
Signals
scroll— emitted for every scroll increment withdx,dyparametersscroll-begin— marks start of a continuous gesture (trackpad)scroll-end— marks end of a continuous gesture
Sign Convention (GTK)
dy > 0= scroll downdy < 0= scroll up- Must negate to match v120 convention (positive = up)
Platform Details — GTK3
Native API
GTK3 uses the legacy GdkEventScroll struct:
⚠️ Important: GTK3 discrete scroll events use theGdkScrollDirectionenum (GDK_SCROLL_UP,GDK_SCROLL_DOWN,GDK_SCROLL_LEFT,GDK_SCROLL_RIGHT), NOTdelta_x/delta_y. Thedelta_x/delta_yfields are only populated whendirection == GDK_SCROLL_SMOOTH.From the GTK3 docs: "Some GDK backends can also generate 'smooth' scroll events, which can be recognized by the
GDK_SCROLL_SMOOTHscroll direction. For these, the scroll deltas can be obtained withgdk_event_get_scroll_deltas()."
// GTK3: Must handle BOTH discrete and smooth
void on_scroll(GtkWidget *widget, GdkEventScroll *event) {
double dy = 0;
if (event->direction == GDK_SCROLL_SMOOTH) {
// Smooth: use delta values (trackpad, high-res mouse)
gdk_event_get_scroll_deltas((GdkEvent*)event, NULL, &dy);
} else {
// Discrete: map direction enum to ±1.0
switch (event->direction) {
case GDK_SCROLL_UP: dy = -1.0; break;
case GDK_SCROLL_DOWN: dy = 1.0; break;
default: break; // LEFT/RIGHT = horizontal
}
}
int wheelDelta = (int)round(-dy * 120.0);
}Platform Details — libinput (Low-Level Linux)
Native API
// Event type: LIBINPUT_EVENT_POINTER_SCROLL_WHEEL
double value = libinput_event_pointer_get_scroll_value_v120(
event, LIBINPUT_POINTER_AXIS_VERTICAL);
// value: already in v120 units! 120 = one notch.
// High-res mice: 30, 60, etc. (fractions of 120)Raw Values
| Device | v120 value | Notes |
|---|---|---|
| Standard mouse notch | ±120 | Already v120! |
| High-res mouse (¼ notch) | ±30 | Sub-notch at higher frequency |
| High-res mouse (½ notch) | ±60 | Sub-notch at higher frequency |
libinput Sign Convention vs Windows
⚠️ Important: The libinput documentation explicitly states: "The v120 value matches the Windows API for wheel scrolling." (libinput wheel-api)Both APIs produce
+120for the same physical gesture (wheel rotated forward/away from user):
- Windows:
+120= "rotated forward, away from the user" = labeled "scroll up" (WM_MOUSEWHEEL)- libinput:
+120= same physical rotation = labeled "scroll down" (libinput pointer API — "positive direction being down")The labels differ (Windows uses user perspective "scroll up"; libinput uses content/axis direction "down") but the sign and magnitude are identical for the same physical gesture. For direct libinput access, no negation is needed — values are already v120-compatible.
However, GTK applies its own sign convention on top of libinput (GTK
dy > 0= scroll down), so the GTK formularound(-dy × 120)does require negation. The negation accounts for GTK's sign convention, not libinput's.
Backend Differences: Wayland vs X11
⚠️ Note: GTK code must handle both display backends gracefully:
- X11: Scroll events are traditionally delivered as button press events (buttons 4/5 for vertical, 6/7 for horizontal). GTK translates these to
GDK_SCROLL_UP/GDK_SCROLL_DOWNdiscrete events with ±1 integer steps. Smooth scrolling support is limited and driver-dependent.- Wayland: Natively supports both discrete (mouse wheel detents) and continuous smooth scrolling (touchpads) via axis events with continuous float values. Different compositors (Mutter, KWin, Sway) may scale values slightly differently.
GTK4's
EventControllerScrollabstracts most of this, but testing on both backends is recommended. See GTK4 X11 backend and GTK4 Wayland backend.
Official Documentation
GTK4:
- EventControllerScroll class — scroll controller with discrete/smooth handling
- EventControllerScroll::scroll signal — dx, dy parameter documentation
- EventControllerScroll::scroll-begin — continuous gesture start
- EventControllerScrollFlags — VERTICAL, HORIZONTAL, DISCRETE, KINETIC
GTK3:
- GdkEventScroll — legacy scroll event struct with direction enum AND delta fields
- GdkScrollDirection — UP, DOWN, LEFT, RIGHT, SMOOTH
- gdk_event_get_scroll_deltas() — retrieves smooth scroll deltas
libinput:
- libinput Wheel scrolling (v120 API) — explains v120 normalization, high-res wheels, and why 120 was chosen
- libinput Pointer events API —
get_scroll_value_v120()function reference, sign convention
Normalization Logic
// GTK4 EventControllerScroll:
// GTK normalizes: mouse notch = ±1.0, trackpad = fractional
int wheelDelta = (int)Math.Round(-dy * 120.0);
// Negate: GTK positive = down, v120 positive = up
// GTK3: Must check direction first (see GTK3 section above)
// Only use delta_y when direction == GDK_SCROLL_SMOOTH
// libinput v120 (if accessible directly):
int wheelDelta = (int)v120value;
// Direct passthrough — libinput v120 matches Windows v120 sign convention.
// Both produce +120 for wheel-forward (away from user).Expected Results
| Input | Source | Calculation | WheelDelta |
|---|---|---|---|
| Mouse notch up (dy=-1.0) | GTK4 | round(-(-1.0) × 120) |
120 |
| Mouse notch down (dy=1.0) | GTK4 | round(-(1.0) × 120) |
-120 |
| Trackpad micro (dy=-0.1) | GTK4 | round(-(-0.1) × 120) |
12 |
| GTK3 discrete UP | GTK3 | round(-(-1.0) × 120) |
120 |
| GTK3 discrete DOWN | GTK3 | round(-(1.0) × 120) |
-120 |
| libinput wheel forward/away (+120) | libinput | passthrough | 120 |
| libinput wheel backward/toward (-120) | libinput | passthrough | -120 |
| High-res ¼ notch forward (+30) | libinput | passthrough | 30 |
Implementation Notes
GTK4 (primary target — view already exists)
SKDrawingAreaexists atsource/SkiaSharp.Views/SkiaSharp.Views.Gtk4/SKDrawingArea.cs(merged in Add SkiaSharp.Views.Gtk4 using GirCore.Gtk-4.0 bindings #3527)- Uses
GirCore.Gtk-4.0v0.7.0, namespaceSkiaSharp.Views.Gtk, targets$(TFMCurrent) - Currently only handles rendering (via
SetDrawFunc→ Cairo surface → SkiaSharp). No input events. - To add scroll: Create a
Gtk.EventControllerScrolland attach to theSKDrawingAreaviaAddController() - The
EventControllerScrollprovidesOnScrollevent withdx/dydoubles - Can distinguish discrete vs smooth via
scroll-begin/scroll-endsignals or theDISCRETEflag - At the GTK level, libinput v120 values are already converted to ±1.0 per notch — so the
× 120formula is correct - Need to add touch handler infrastructure (SKTouchEventArgs, etc.) similar to other platforms
SKDrawingAreahasDispose()override — clean up scroll controller there
GTK3 (legacy — maintain parity)
SKDrawingAreaexists atsource/SkiaSharp.Views/SkiaSharp.Views.Gtk3/SKDrawingArea.cs- Uses old-style
GdkEventScroll— must handle both direction enum AND smooth deltas (see GTK3 section) - Add a
ScrollEventhandler viawidget.Events |= EventMask.ScrollMaskand override/connectScrollEvent
General
- Test on both X11 and Wayland backends to verify consistent behavior
- GTK4 is preferred for new development. GTK3 should maintain parity.
- Priority is lower than MAUI platforms
Metadata
Metadata
Assignees
Labels
Type
Projects
Status