One mouse-wheel notch starts a fling that runs for roughly forty seconds. It reads as a view that cannot be stopped, only reversed, because reversing is the only thing that cancels the velocity.
All references are v0.10.1, checked against main as well.
The arithmetic
ScrollPhysics (src/primitives/canvas/tokens.zig:774) turns a wheel delta into a velocity and then decays it:
wheel_velocity_scale: f32 = 60,
deceleration_per_second: f32 = 0.86,
stop_velocity: f32 = 5,
Velocity retains 86% per second, so the time for one notch to fall under the stop threshold is ln(stop / v0) / ln(0.86).
On Linux that is much worse than it looks, because the GTK host multiplies a wheel notch by 40 before the physics ever sees it (src/platform/linux/gtk_host.c:1503-1508, converting GDK_SCROLL_UNIT_WHEEL to points):
v0 = 1 notch x 40 x 60 = 2400 points/second
stop at 5 = ln(5/2400) / ln(0.86) ~ 41 seconds
Two notches is not two flings, it is one faster one, so ordinary scrolling makes it worse rather than better.
Why it is a wheel problem specifically
These defaults are right for a trackpad, where deltas arrive small and continuous and the decay reads as momentum. A wheel notch is a discrete displacement, not a throw, and it is the same code path with a 40x multiplier in front of it.
Reproduction, headless, no compositor
The automation channel drives the canvas directly, so this needs no window manager and no real input device:
- Build a canvas app with
-Dautomation=true, run it under Xvfb.
native automate widget-wheel <view> <scroll-region-id> 5 (one command, once).
- Snapshot three times over the following fifteen seconds and compare widget bounds.
I measured a feed row moving 400.6 -> 57.6 -> -412.3 -> -885.4 across snapshots at +3s, +9s and +17s, from that single command with no further input. Deceleration is visible (114, then 78, then 59 points/second) which is what confirms it is the decay curve rather than repeated events.
Suggested fix
Treat a wheel notch as a displacement rather than a fling. The unit is already known at the source: gtk_event_controller_scroll_get_unit returns GDK_SCROLL_UNIT_WHEEL for a notch and GDK_SCROLL_UNIT_SURFACE for a trackpad, and the host already branches on exactly that to apply the 40x. Carrying that distinction through to the physics, so a wheel scrolls by its delta and only a surface delta contributes velocity, fixes it without changing how a trackpad feels.
Failing that, the default decay is too slow to be a fling. At 0.86 per second a throw is still moving perceptibly half a minute later. Something nearer 0.05 per second stops in about a second, which is what a fling usually means.
And the 40x and the 60x compound. Whatever else changes, wheel_velocity_scale being applied on top of the host's wheel-to-points conversion is worth a look: one of the two is enough.
The tokens are overridable, so I have set wheel_velocity_scale and deceleration_per_second in my own app to keep it usable, scoped to non-macOS. That is a workaround for one app rather than a fix, since the defaults are what every canvas app gets.
One mouse-wheel notch starts a fling that runs for roughly forty seconds. It reads as a view that cannot be stopped, only reversed, because reversing is the only thing that cancels the velocity.
All references are v0.10.1, checked against
mainas well.The arithmetic
ScrollPhysics(src/primitives/canvas/tokens.zig:774) turns a wheel delta into a velocity and then decays it:Velocity retains 86% per second, so the time for one notch to fall under the stop threshold is
ln(stop / v0) / ln(0.86).On Linux that is much worse than it looks, because the GTK host multiplies a wheel notch by 40 before the physics ever sees it (src/platform/linux/gtk_host.c:1503-1508, converting
GDK_SCROLL_UNIT_WHEELto points):Two notches is not two flings, it is one faster one, so ordinary scrolling makes it worse rather than better.
Why it is a wheel problem specifically
These defaults are right for a trackpad, where deltas arrive small and continuous and the decay reads as momentum. A wheel notch is a discrete displacement, not a throw, and it is the same code path with a 40x multiplier in front of it.
Reproduction, headless, no compositor
The automation channel drives the canvas directly, so this needs no window manager and no real input device:
-Dautomation=true, run it under Xvfb.native automate widget-wheel <view> <scroll-region-id> 5(one command, once).I measured a feed row moving 400.6 -> 57.6 -> -412.3 -> -885.4 across snapshots at +3s, +9s and +17s, from that single command with no further input. Deceleration is visible (114, then 78, then 59 points/second) which is what confirms it is the decay curve rather than repeated events.
Suggested fix
Treat a wheel notch as a displacement rather than a fling. The unit is already known at the source:
gtk_event_controller_scroll_get_unitreturnsGDK_SCROLL_UNIT_WHEELfor a notch andGDK_SCROLL_UNIT_SURFACEfor a trackpad, and the host already branches on exactly that to apply the 40x. Carrying that distinction through to the physics, so a wheel scrolls by its delta and only a surface delta contributes velocity, fixes it without changing how a trackpad feels.Failing that, the default decay is too slow to be a fling. At 0.86 per second a throw is still moving perceptibly half a minute later. Something nearer 0.05 per second stops in about a second, which is what a fling usually means.
And the 40x and the 60x compound. Whatever else changes,
wheel_velocity_scalebeing applied on top of the host's wheel-to-points conversion is worth a look: one of the two is enough.The tokens are overridable, so I have set
wheel_velocity_scaleanddeceleration_per_secondin my own app to keep it usable, scoped to non-macOS. That is a workaround for one app rather than a fix, since the defaults are what every canvas app gets.