Skip to content

Replace QScroller with custom KineticScroller.#327

Open
poli0iq wants to merge 7 commits into
desktop-app:masterfrom
poli0iq:kinetic-scroller
Open

Replace QScroller with custom KineticScroller.#327
poli0iq wants to merge 7 commits into
desktop-app:masterfrom
poli0iq:kinetic-scroller

Conversation

@poli0iq

@poli0iq poli0iq commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

QScroller has:

  1. A bad inertia curve (all of the default ones).
  2. A bad initial acceleration estimation.

These can't be properly tuned, so the only option is to reimplement QScroller.

This implementation's maths is based on Chromium and GTK4.

This also replaces the screen refresh rate heuristics with proper frame scheduling (vsync-accurate).

KineticScroller also exposes velocity directly, allowing more precise overscroll behavior without relying on estimated velocity from rounded per-tick deltas (fixing the spring suppression in the slow fling tails).

Comment thread ui/widgets/scroll_area.cpp Outdated
Comment on lines +503 to +506
if (OptionKineticScroller.value()) {
_scroller = new KineticScroller(this);
} else if (_scroller) {
QObject deleter;
_scroller->setParent(&deleter);
delete _scroller;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have access to constructor and destructor then use a smart pointer

Suggested change
if (OptionKineticScroller.value()) {
_scroller = new KineticScroller(this);
} else if (_scroller) {
QObject deleter;
_scroller->setParent(&deleter);
delete _scroller;
_scroller = OptionKineticScroller.value()
? std::make_unique<KineticScroller>(this);
: nullptr;

Comment thread ui/widgets/scroll_area.cpp Outdated
_wheelPos,
crl::now());
}
// The press event's own delta counts too: a short fling

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we're resurrecting ScrollBegin's delta here while ignoring it multiple lines above if we're in KineticScroller::Scrolling state?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are different deltas. The one ignored above is the leaked macOS momentum stream (qnsview_mouse.mm can emit ScrollBegin straight from momentumPhase == Began when it misses the handoff), which we're already simulating ourselves.

On Wayland, though, ScrollBegin always has a zero delta, so it's the first ScrollUpdate that triggers the press, and its delta is a velocity sample we shouldn't drop.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How it happens ScrollUpdate triggers the press? ScrollBegin should trigger InputPress and the next ScrollUpdate triggers InputMove.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_wheelPos being null means no gesture is open. Normally ScrollBegin opens it with InputPress, then each ScrollUpdate sends InputMove. But the first event we see can be a ScrollUpdate, for example when the check above swallowed a real begin during a fling. In that case wasNull is true on the update, so the press comes from it. And unlike a begin, the update carries a real scroll delta, which we then send as the first InputMove: a short fling has only a few velocity samples, dropping one would noticeably weaken it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the first event we see can be a ScrollUpdate, for example when the check above swallowed a real begin during a fling.

You just said ScrollBegin always has zero delta on Wayland? If that's right, then this change has zero effect and should be reverted. If not, then remove the macOS workaround as macOS users aren't the expected users of this feature anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I've debugged raw events and this path is indeed useless, the problem was related to Bluetooth buffering some events (leading to losing some short flings because we use crl::now() timestamps instead of QWheelEvent::timestamp()).

(yeah I use a bluetooth touchpad)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crl::now() was used only because QFlickGestureRecognizer uses QElapsedTimer so I mirrored it. If event timestamps are in fact better then we can use them.

Comment thread ui/widgets/elastic_scroll.cpp Outdated
Comment on lines 436 to 440
if (OptionKineticScroller.value()) {
_scroller = new KineticScroller(this);
} else if (_scroller) {
QObject deleter;
_scroller->setParent(&deleter);
delete _scroller;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Smart pointer too.

Suggested change
if (OptionKineticScroller.value()) {
_scroller = new KineticScroller(this);
} else if (_scroller) {
QObject deleter;
_scroller->setParent(&deleter);
delete _scroller;
}
_scroller = OptionKineticScroller.value()
? std::make_unique<KineticScroller>(this);
: nullptr;

@poli0iq
poli0iq force-pushed the kinetic-scroller branch 3 times, most recently from 4ee73c5 to a7ed8d7 Compare July 22, 2026 15:03
QObject deleter;
_scroller->setParent(&deleter);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could the empty line be returned?

QObject deleter;
_scroller->setParent(&deleter);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here

@ilya-fedin

ilya-fedin commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Last stylistic thing I see right now: both new .cpp and .h seem to use spaces rather than tabs.

And to technical things:

  1. An annoying thing with QScroller is that it works in absolute coordinates, we have to convert deltas to absolute positions and back, just to then bug on loading more messages. Given that we change it, couldn't we get rid of this too and just directly operate on delta? Or is it crucial to the logic to know scroll area size and position?
  2. If possible, I believe it would be nice to get rid of the handleWheelEvent/handleScrollEvent separation just like it was before QScroller introduction. We had to use scroll events with it, but we're not forced anymore with an in-tree API I believe.
  3. Is there any sense to have separate ScrollerStopper now or would it be more effectively done by KineticScroller itself?

@poli0iq
poli0iq force-pushed the kinetic-scroller branch from a7ed8d7 to 72cac0f Compare July 23, 2026 20:48
poli0iq added 7 commits July 24, 2026 00:15
QScroller has:
1. A bad inertia curve (all of the default ones).
2. A bad initial acceleration estimation.

These can't be properly tuned, so the only option is to reimplement
QScroller.

This implementation's maths is based on Chromium and GTK4.
QScroller didn't expose it, so the only option was tracking the velocity
from (rounded to integer) per-tick deltas, which read zero in the slow
tail of a fling and suppressed the spring.

The KineticScroller's velocity can be exposed and is exact at any
instant, so use it instead.
Absolute positions came from QScroller. This was actually inconvenient,
because we had to convert deltas to absolute positions and back,
and rebase whenever loading messages moved the content.
The separation existed only for QScroller.
This only existed because QScroller couldn't watch mouse events itself.
KineticScroller already event-filters its frame window, so stopping the
fling on real pointer activity belongs to its Scrolling state.
@poli0iq
poli0iq force-pushed the kinetic-scroller branch from 72cac0f to 3ca8239 Compare July 24, 2026 10:11
@poli0iq

poli0iq commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Last stylistic thing I see right now: both new .cpp and .h seem to use spaces rather than tabs.

I don't think they do.

And to technical things:

  1. An annoying thing with QScroller is that it works in absolute coordinates, we have to convert deltas to absolute positions and back, just to then bug on loading more messages. Given that we change it, couldn't we get rid of this too and just directly operate on delta? Or is it crucial to the logic to know scroll area size and position?

  2. If possible, I believe it would be nice to get rid of the handleWheelEvent/handleScrollEvent separation just like it was before QScroller introduction. We had to use scroll events with it, but we're not forced anymore with an in-tree API I believe.

  3. Is there any sense to have separate ScrollerStopper now or would it be more effectively done by KineticScroller itself?

All three implemented (as separate commits for now, lmk if I should fixup).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants