Skip to content

Add wheel support to GTK3/GTK4 views with v120 normalization #3540

@mattleibow

Description

@mattleibow

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/) — extends Gtk.DrawingArea, only handles PaintSurface, no input events
  • GTK4 SKDrawingArea (source/SkiaSharp.Views/SkiaSharp.Views.Gtk4/) — extends Gtk.DrawingArea using GirCore.Gtk-4.0 (v0.7.0), renders via SetDrawFunc callback. Handles PaintSurface only — no scroll or input events yet. Merged in Add SkiaSharp.Views.Gtk4 using GirCore.Gtk-4.0 bindings #3527.
  • Neither has any SKTouchHandler or 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.EventControllerScroll via the GirCore.Gtk-4.0 NuGet package. Linux is unique in that the underlying libinput library 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 SKDrawingArea in source/SkiaSharp.Views/SkiaSharp.Views.Gtk4/ uses SkiaSharp.Views.Gtk namespace with GirCore.Gtk-4.0 v0.7.0. The Gtk.EventControllerScroll class 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 with dx, dy parameters
  • scroll-begin — marks start of a continuous gesture (trackpad)
  • scroll-end — marks end of a continuous gesture

Sign Convention (GTK)

  • dy > 0 = scroll down
  • dy < 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 the GdkScrollDirection enum (GDK_SCROLL_UP, GDK_SCROLL_DOWN, GDK_SCROLL_LEFT, GDK_SCROLL_RIGHT), NOT delta_x/delta_y. The delta_x/delta_y fields are only populated when direction == GDK_SCROLL_SMOOTH.

From the GTK3 docs: "Some GDK backends can also generate 'smooth' scroll events, which can be recognized by the GDK_SCROLL_SMOOTH scroll direction. For these, the scroll deltas can be obtained with gdk_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

libinput v120 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 +120 for 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 formula round(-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_DOWN discrete 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 EventControllerScroll abstracts most of this, but testing on both backends is recommended. See GTK4 X11 backend and GTK4 Wayland backend.

Official Documentation

GTK4:

GTK3:

libinput:

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)

  • SKDrawingArea exists at source/SkiaSharp.Views/SkiaSharp.Views.Gtk4/SKDrawingArea.cs (merged in Add SkiaSharp.Views.Gtk4 using GirCore.Gtk-4.0 bindings #3527)
  • Uses GirCore.Gtk-4.0 v0.7.0, namespace SkiaSharp.Views.Gtk, targets $(TFMCurrent)
  • Currently only handles rendering (via SetDrawFunc → Cairo surface → SkiaSharp). No input events.
  • To add scroll: Create a Gtk.EventControllerScroll and attach to the SKDrawingArea via AddController()
  • The EventControllerScroll provides OnScroll event with dx/dy doubles
  • Can distinguish discrete vs smooth via scroll-begin/scroll-end signals or the DISCRETE flag
  • At the GTK level, libinput v120 values are already converted to ±1.0 per notch — so the × 120 formula is correct
  • Need to add touch handler infrastructure (SKTouchEventArgs, etc.) similar to other platforms
  • SKDrawingArea has Dispose() override — clean up scroll controller there

GTK3 (legacy — maintain parity)

  • SKDrawingArea exists at source/SkiaSharp.Views/SkiaSharp.Views.Gtk3/SKDrawingArea.cs
  • Uses old-style GdkEventScrollmust handle both direction enum AND smooth deltas (see GTK3 section)
  • Add a ScrollEvent handler via widget.Events |= EventMask.ScrollMask and override/connect ScrollEvent

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

No one assigned

    Type

    No type

    Projects

    Status

    New

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions