Uploaded image for project: 'Qt'
  1. Qt
  2. QTBUG-129948

Flickable's mouse wheel scrolling behavior needs improvement

    XMLWordPrintable

Details

    • Suggestion
    • Resolution: Unresolved
    • Not Evaluated
    • None
    • 6.8
    • Quick: Controls 2
    • None
    • Linux/X11, Windows

    Description

      Currently, Flickable in Qt Quick 2 comes with a built-in smooth scrolling animation, which is good. Qt 6.8 now also has FluentWinUI3 style, which is also good.

      However, the current smooth scrolling animation behavior is problematic in many ways comparing to WinUI ( WinUI 3 Gallery link ).

      1. Scrolling speed is too slow

      A mouse with unlockable wheel can scroll the content continuously with the inertia. When using such mouse, it's very easy to notice that, in Qt Quick applications, the scrolling speed of Flickable is throttled to a certain velocity. It feels as if:

      • (less likely) Either when the mouse wheel event comes in too frequent, some of them get dropped;
      • (more likely) Or that the target scroll position is based on current position + fixed delta, and therefore no matter how fast the event comes in, the scrolling speed is capped because the animation speed is capped.

      The scrolling target position should not base on current position when wheel event is fired repeatedly as it'll cap the scrolling speed, instead the wheel event should directly change the target position.

      2. Counterintuitive overscroll animation when hitting the edge

      Flickable has an overscroll animation which bounces at the edge and goes back. This kind of animation makes sense with a touchpad or touchscreen, however it goes against the common sense for a mouse wheel. Specifically, none of the other major GUI toolkits/implementations do this.

      • WinUI: always gracefully stops at the edge; overscroll animation is only active for touchpad/touchscreen
      • Chromium/Electron: overall feeling is very similar to WinUI.
      • Firefox: overall feeling is very similar to WinUI.
      • Win32/MFC: no smooth scrolling at all; stops at the edge; no overscroll
      • KDE apps w/ QWidgets: Has smooth scrolling animation; stops at the edge; no overscroll
      • GTK: Smooth scrolling animation only for touchpad/touchscreen; no overscroll for mouse wheel

      Flickable should not overscroll with a mouse wheel. The animation should be active only when using a touchpad/touchscreen.

      3. Doesn't respect system animation toggle state

      Windows disables UI animations over a remote session to save bandwidth, KDE Plasma has an animation speed slider, which when adjusted to instant, essentially turns off the animation. However, none of them are respected by Flickable. The current KDE implementation of animation toggle basically reimplements the whole animation, and then setting the custom animation duration to 0 to pretend as the turned off state ( relevant source ).

       

      Flickable should respect system animation settings, or (more favorably) the system animation toggle state should be easily accessible within QML/C++/PySide6 and the Flickable scrolling animation can be turned off with just a property.

       


       

      I've played around the animation for a few hours and came up with an implementation that reassembles the feeling of WinUI:

      // SmoothScroll.qml
      import QtQuick
      
      Item {
          id: root
          readonly property Flickable target: parent.parent as Flickable
          // If casting failed, don't fill the parent
          // (which blocks the mouse wheel events),
          // let the underlying item handle mouse wheel events instead.
          anchors.fill: !!target ? parent : null
          readonly property real minContentX: 0
          // Math.max is used here to prevent getting a negative horizontal position.
          readonly property real maxContentX: Math.max(target.contentWidth - target.width, 0)
          readonly property real minContentY: 0
          readonly property real maxContentY: Math.max(target.contentHeight - target.height, 0)
          function getBoundedX(x) {
              if (x <= minContentX)
                  return minContentX;
              else if (x >= maxContentX)
                  return maxContentX;
              else
                  return x;
          }
          function getBoundedY(y) {
              if (y <= minContentY)
                  return minContentY;
              else if (y >= maxContentY)
                  return maxContentY;
              else
                  return y;
          }
      
          property real targetContentX
          property real targetContentY
          property bool animatingX
          property bool animatingY
      
          // When flickable is not moving by the custom animation,
          // bind the targetContentX/Y to the actual ContentX/Y.
          // Otherwise, the animation should take care of
          // updating the contentX/Y to match targetContentX/Y.
          Binding {
              root.targetContentX: root.target.contentX
              when: !root.animatingX
              restoreMode: Binding.RestoreBinding
          }
          Binding {
              root.targetContentY: root.target.contentY
              when: !root.animatingY
              restoreMode: Binding.RestoreBinding
          }
          WheelHandler {
              id: mouse
              // Only want mouse here, touchpad scrolling should be untouched.
              acceptedDevices: PointerDevice.Mouse
              // Boilerplates
              onWheel: event => {
                  const old_targetContentX = root.targetContentX;
                  const old_targetContentY = root.targetContentY;
                  const deltaX = event.angleDelta.x / -2;
                  const deltaY = event.angleDelta.y / -2;
                  let new_targetContentX;
                  let new_targetContentY;
      
                  if (event.modifiers & Qt.ShiftModifier) {
                      new_targetContentX = root.targetContentX + deltaY;
                      new_targetContentY = root.targetContentY + deltaX;
                  } else if (!event.modifiers) {
                      new_targetContentX = root.targetContentX + deltaX;
                      new_targetContentY = root.targetContentY + deltaY;
                  } else if (event.modifiers & Qt.ControlModifier) {
                      event.accepted = false;
                      return;
                  } else {
                      event.accepted = false;
                      return;
                  }
      
                  new_targetContentX = root.getBoundedX(new_targetContentX);
                  new_targetContentY = root.getBoundedY(new_targetContentY);
                  if (new_targetContentX != old_targetContentX) {
                      root.targetContentX = new_targetContentX;
                      animationX.restart();
                  }
                  if (new_targetContentY != old_targetContentY) {
                      root.targetContentY = new_targetContentY;
                      animationY.restart();
                  }
              }
          }
          NumberAnimation {
              id: animationX
              target: root.target
              properties: "contentX"
              duration: 300
              from: root.target.contentX
              to: root.targetContentX
              // stop() by default stops the animation.
              // This prevents that from happening.
              alwaysRunToEnd: true
      
              // OutQuad closely replicates WinUI style scrolling animation
              easing.type: Easing.OutQuad
              onStarted: {
                  root.animatingX = true;
              }
              onStopped: {
                  root.animatingX = false;
              }
          }
          NumberAnimation {
              // The same animation for Y axis
              id: animationY
              target: root.target
              properties: "contentY"
              duration: 300
              from: root.target.contentY
              to: root.targetContentY
              alwaysRunToEnd: true
      
              easing.type: Easing.OutQuad
              onStarted: {
                  root.animatingY = true;
              }
              onStopped: {
                  root.animatingY = false;
              }
          }
      }
      
      

      It can be used like this:

      Flickable {
        ...
        SmoothScroll {} // activates the alternate scrolling animation behavior
      }

      Since this implementation is done in only a few hours, I don't expect it to be bullet-proof mature, and it doesn't handle system animation toggle state. However, the point is that QML is clearly capable of emulating WinUI's scrolling animation behavior with just the existing building blocks without too much effort.

      Flickable's scrolling animation should match the WinUI behavior at least on Windows to emulate the platform behavior. Alternatively the scrolling animation behavior should be customizable similar to the control style.

      Attachments

        Issue Links

          No reviews matched the request. Check your Options in the drop-down menu of this sections header.

          Activity

            People

              qt.team.quick.subscriptions Qt Quick and Widgets Team
              jirauser69920 user-6dbf8 (Inactive)
              Votes:
              4 Vote for this issue
              Watchers:
              7 Start watching this issue

              Dates

                Created:
                Updated:

                Gerrit Reviews

                  There are no open Gerrit changes