View Issue Details
|ID||Project||Category||View Status||Date Submitted||Last Update|
|0038205||Lazarus||Widgetset||public||2020-12-11 21:08||2021-02-09 23:58|
|Reporter||Daniel||Assigned To||Dmitry Boyarintsev|
|Summary||0038205: macOS: OnMouseWheel is less precise than it is on Windows, leading to bad scrolling behavior|
|Description||In the old times, MouseWheel only passed WheelDelta of -120 / +120 to applications. This was fine for old-fashioned mouse-wheels, but isn't sufficient for modern trackpads and some modern mouse wheels.|
Windows nowadays gives more fine grained values to applications, making scrolling a lot smoother (if supported by hardware). This works well in Lazarus. onMouseWheel gives values to applications that sometimes go down to a single increment, allowing pixel-perfect scrolling.
On macOS, good scrolling hardware is much more widespread, but unfortunately it seems like the FCL turns the nice events into just -120/+120. I assume this was done for compatibility with code written for Windows, but given that Windows itself has moved on from coarse events, I think it would be a good idea to also use the finer events on macOS. This would make scrolling feel a lot better.
- macOS: https://youtu.be/7M21AecxM8Y
- Windows: https://youtu.be/b_2IgWjeWcY
|Steps To Reproduce||Run the attached sample project on Windows and macOS. Notice how it scrolls pretty well on Windows (given the right hardware) but poorly on macOS:|
- No pixel-perfect scrolling
- Scrolling is generally too fast, unless...
- ...we scroll REALLY fast, in which case maximum scrolling speed is too low
Notice in particular how WheelDelta is never anything else than +/- 120.
|Additional Information||- This also affects the Lazarus IDE itself. Scrolling is quite poor on macOS.|
- If anyone wants to reproduce this on Windows, note that driver support is spotty:
* Microsoft Precision Touchpads work well (I tried a Surface Pro X which works perfectly)
* Logitech mice (e.g. MX Master 3) support fine events but their drivers don't seem to hand them out (unless they rename themselves to Firefox.exe :-) ).
Regardless of spotty driver support, Windows apps have to be able to deal with values <>120 as they will get those values on some systems.
|Tags||No tags attached.|
|Fixed in Revision|
LazarusScrollingRepro.zip (130,306 bytes)
I looked into patching this. A pretty simple point fix vastly improves the current scrolling:
CocoaWSCommon, in TLCLCommonCallback.scrollWheel:
if dy <> 0 then
Msg.Msg := LM_MOUSEWHEEL;
Msg.WheelDelta := Round(dy * 3);
if dx <> 0 then
Msg.Msg := LM_MOUSEHWHEEL;
// see "deltaX" documentation.
// on macOS: -1 = right, +1 = left
// on LCL: -1 = left, +1 = right
Msg.WheelDelta := Round(-dx);
Why 3? This offsets the 3 that SystemParametersInfo(SPI_GETWHEELSCROLLLINES) returns (see my example program attached). A proper fix should probably call that function instead.
With this, scrolling is vastly better. It works properly for both Mouse and TrackPad, including momentum. You can scroll pixel perfect or fast. Basically, it behaves like any other mac program.
unlike Windows API, macOS scroll wheel was float-point based from the start.
I don't see how SPI_GETWHEELSCROLLLINES parameter relates to the returned WheelDelta value
MSDN states "Retrieves the number of lines to scroll when the vertical mouse wheel is moved. The pvParam parameter must point to a UINT variable that receives the number of lines. The default value is 3."
How does SynEdit behaves after the patch applied? (scrolling compared between macOS and Windows)
||please try 64285|
So I think there are two things here: 1) SPI_GETWHEELSCROLLLINES and 2) event accuracy. I'll discuss them separately, as even if we disagree on 1, 2 still makes sense (and is the more important one anyway).
From reading Microsoft's docs on this matter, my understanding is this:
Back in the days when 120 was returned, 120 was supposed to be seen as "one click" on the mouse wheel. Applications should then scroll by SPI_GETWHEELSCROLLLINES. So if e.g. SPI_GETWHEELSCROLLLINES=3, then 120 means "scroll by 3 lines".
As mouse wheels became more accurate they returned smaller values than 120. But still 120 should correspond to SPI_GETWHEELSCROLLLINES. So the compatible way of handling this is to calculate the number of lines by dividing: ScrollAmount / ValueOf(SPI_GETWHEELSCROLLLINES). This way old mice behave the same way as before, but new mice gain extra accuracy.
on macOS, SPI_GETWHEELSCROLLLINES is a constant in LCL. I think it makes sense to pre-scale the values given by the OS so that client code can use a single formula for all OSes. An alternative would be make SPI_GETWHEELSCROLLLINES return 1.
2. I'm assuming SynEdit is the editor used in the Lazarus IDE itself? I applied this to the IDE itself and been living with it for a week. Scrolling is much improved, but not perfect:
- Before this patch, scrolling was completely off for me. Both when using the trackpad or when using the mouse wheel, SynEdit would scroll way too much. Move your fingers just a tiny bit on the trackpad and the editor shoots away.
- With this patch applied, the scale of the scroll is much improved. Can't say for certain it's perfect, but at least I am no longer scared to scroll and see everything run away into the darkness :-)
- To make it perfect, SynEdit itself needs to change how it scrolls. It currently is line-based, meaning there is a "top most visible line". For proper scrolling on macOS (and Windows with newer hw), SynEdit would need to change to pixel-based scrolling. Not something that can be fixed on this layer, but at least sending out proper events enables higher level controls to do the right thing :)
In general, this makes the behavior much closer to what happens on Windows (with precision touchpad drivers).
The Wheel Delta of 120 is a fixed max value, it should never exceed this value..
The idea was that if ever in the future mouse makers or devices like mice ever decided to make finer wheels on the mouse instead of the normal indent we have now, they can reduce this wheel delta to show that the user is performing a smooth scroll and a full single notch is not completed until 120 is summed up.
The "scroll wheel lines" is all about text viewable content that have scrollbars, that actually makes the scrollbar move to equal the amount of 3 lines verses what the font size is.
so basically 120 is the magic peek number before a single mouse wheel click has been done and most mice that is what the OS reports on a single wheel click which should result in a single pixel value response.
For apps that have special graphics programs this value could be much less to indicate the mouse device is much finer in res but it should all sum to 120 before its considered a normal mouse wheel move.
120 is easily exceed with a touchpad, as it tries to emulate "physical scroll inertia"
and even a regular mouse wheel shows 240 or -240 in windows
120 is not a cap in any means
how are you getting 240 ?
Please tell me how you derived at that since MS clearly states otherwise.
Please read that below..
it clearly states 120 to be the max and your 240 you are getting I also got when I was doing it incorrectly ..
in windows if you just display the incoming Delta value directly you get 120..
finer mouse product less Delta values not more..
i did attach a sample test for you.
run the app and wheel as crazy over the window!
Should work fine (and show 240 value) with a regular mouse. Might get a greater variety if a touch pad is used.
The page doesn't state anything about maximum value, it only states that the value would be in multiples or divisions of 120.
wheeldelta.zip (2,025 bytes)
Ok, I have a touch screen here and it works ok, reports =/- 120
I need to charge my Windows 10 tablet so that will take a bit..
got enough juice in the tablet, its a 32 but RCA Windows 10. .
I get -120 or 120
so I don't understand how you are getting that when MS clearly states those values don't exceed 120 but get less for higher res.
also looking around I can't seem to find any reference API calls or system settings to change that either, also not even to require the current value unless its in some most recent updates of windows?
a simple scroll swipe on my HP Envy laptop touchpad gives the me following list:
I don't know what to tell you..
it does not do that here.
I get only 120 or -120
I can swipe my scroll all day scrolling that thing around and that is what I get.
in all reality it really does not matter anyways what you get there for a value, its the fact that you got there.
if its 120 or more then you have reached the requirement of a single scroll of the mouse wheel.
I looked high and low in the net for your issue and can't find any reference to it..
as Daniel said it's driver/hardware specific behavior. Your hardware only allows you to get +-120
The only question is how to map Cocoa's float-point wheel event value to LCL's (Windows) 120 Wheel Delta.
For the best results, it should be physically the same hardware tested on both systems.
please revert your patch and try r64285, thanks
I found the settings on my Tablet for the mouse but it only allows me like 20..120 lines for the indent on the mouse wheel.
its obvious you are getting the minimum scroll lines to satisfy the case.
I don't know what cocoa generates for a value but basically the LCL does not care about the value because there isn't any thing in there that uses the specific value, all it needs to know is at the moment is it - or + number coming in and that will report which direction its moving..
User code on the other hand can use that value if they are aware of its importance and also them knowing their specific hardware in use.
so I wouldn't put too much time into determining what values are correct for the LCL, its just + or - when the event arrives for the time being..
and as for the scrollLines = 3, that is not directly related but I guess you could integrate It in..
If you feel like you absolutely need to work with the value in other parts of the widget then I would say a multiple of (120) *3 for the scrollbar scrolling range
Trunc(InputValueReceived / 120) * 3 or what ever the default scrollbar range is. But this should only be used for scrollbar info.
> I don't know what cocoa generates for a value but basically the LCL does not care
> about the value because there isn't any thing in there that uses the specific value,
> all it needs to know is at the moment is it - or + number coming in and that will
> report which direction its moving..
This whole issue is actually about that!
The end users cares about the sensitivity of the wheel (or a device that sends wheel actions).
It's not just the moment -120 or +120 because CocoaWS already does it.
It's now about the right wheel delta value.
Dmitry, I think we're in agreement just a small note:
"For the best results, it should be physically the same hardware tested on both systems".
macOS and Windows have - amongst other things - different acceleration curves. Small scrolling increments on macOS tend to be much smaller, whereas the maximum speed is higher. So I think the goal shouldn't necessarily to have Windows and macOS behave identically but to have both behave naturally (meaning: as other apps behave). I think all we have to do here is to scale the incoming value.
Attempting to checkout and test r64285 right now.
Tried out your patch. Thanks first of all for taking the time to fix this!
However, the scaling is very off: With it, everything scrolls way too much. It seems that dy is never smaller than 1 (I think it is meant to be the number of pixels@1x to scroll), so we can't just turn that into 120 (which roughly means "1 line").
Played around with different factors and compared with other programs: It seems that 3 to 4 is about the right scale. I am not sure why that would be the case however.
if isPrecise then
Msg.WheelDelta := Round(dy * 3)
Msg.WheelDelta := sign(dy) * LCLStep;
With this scale, the Lazarus IDE itself is pretty usable. And pixel-perfect scrolling controls (like the test program I attached) work really well.
I can't test the isPrecise=false case (I don't know if macOS at this point ever generates these events any longer), but according to the docs (https://developer.apple.com/documentation/appkit/nsevent/1535387-scrollingdeltay?language=objc), we should do "Round(dy * LCLStep)" here. But then again, can't test it :-(
it does isPrecise=false for me all the time.
I'm using a "regular" mouse.
Oh I understand now. I just tried an older Logitech gaming mouse (which doesn't have a precise scroll wheel) and scrolling is choppy but quite usable in Lazarus.
Using a MacBook Pro trackpad or a Logitech MX Master 3 (with Logitech drivers installed) however, things are very different: Where other mac apps are super smooth, LCL apps aren't great.
Do you by any chance have access to hardware like that?
i purchased Corsair M65 gaming mouse. However it is showing a regular mouse wheel behavior. (I didn't try Corsair software. Maybe there's a calibration option)
I do have MacBook Pro mid 2010. I didn't try it yet, but I doubt it has "precise" touchpad (too old of the version).
If 2010 doesn't have the desired sensitivity, I might need to get MX Master 3.
Just FYI, for the Logitech mouse you need their software. Standard USB hid doesn't support fine mouse wheel movement unfortunately, so they implement fine scrolling through accessibility APIs. If you're ok with granting those permissions, the mouse works well.
I believe the 2010 already had precise scrolling mechanics, so that might just work. Another option would be to use a Bluetooth Apple Trackpad. Probably any old one would do.
Just to add another data point - with Big Sur/M1 processor/Logitech MAX master/current SVN the lazarus scrolling is too much - I suspect a scale factor of 40 (=120/3?) is needed on the number of lines. The same mouse works fine on lazarus on linux.
With smooth scrolling enabled using the Logitech options program a similar scale factor is needed, though now the minimum amount I can scroll by is 3-5 lines (a small fraction of a wheel "click").
Note that I think the OS (or some libraries) can combine clicks and may deliver the sum of several wheel clicks as a single message, rather than individually, particularly if a lot of them arrive in a short period.
Some extra input on Windows behavior. (standard drivers)
There's a setting "number of lines to scroll". The setting doesn't affect the system reported wheel delta (which remains +/-120)
It only changes the scroll behavior for SOME controls (or applications).
i.e. Windows Explorer does respect the value, SynEdit - ignores it.
Ultimately I don't see how this "magic 3" should be used related to macOS. and if there's a reason to say "we should apply factor of 3".
It might feel right, but I'd like to get a more technical reason except for subjective experience.
I did try Dell touchpad (on Dell Precision M6800) on Win 10
Wheel delta reported is "any number", not bound to 120 in any manner.
My reading of the following code in gtk2callback.inc
case event^.direction of
GDK_SCROLL_UP: begin MessE.Msg := LM_MOUSEWHEEL; MessE.WheelDelta := 120; end;
GDK_SCROLL_DOWN: begin MessE.Msg := LM_MOUSEWHEEL; MessE.WheelDelta := -120; end;
is that for gtk2 the assumption that 1 wheel click = 120 is baked into the code. Grepping for 120 in the widgetset directory yields the following:
interfaces/carbon/carbonprivatewindow.inc: // LCL expects the delta to be 120 for each wheel step, which should scroll
interfaces/carbon/carbonprivatewindow.inc: // Update: 20111212 by zeljko: All widgetsets sends WheelDelta +-120
interfaces/qt/qtwidgets.pas: // LCL expects delta +-120, we must fix it. issue 0020888
interfaces/qt/qtwidgets.pas: Msg.WheelDelta := (120 * Msg.WheelDelta) div Mouse.WheelScrollLines;
interfaces/qt/qtwidgets.pas: Msg.WheelDelta := 120;
so I think 120 is baked into the LCL.
120 is baked in WinAPI.
LCL is a cross platform WinAPI emulator, so the basic implementation goes by 120.
I think there's a fundamental mismatch in what those numbers represent.
Windows originally defined:
120 equals x rows (with x=SPI_GETWHEELSCROLLLINES, which is the Control Panel setting that Dmitry posted a screenshot of).
On Windows, 120 is no longer necessarily a given, but the question how to interpret the scroll value now. For example, what does 60 mean? I see two possible interpretations:
- 60 means 1/2 of x rows
- 60 means 60*f pixels (with f being some platform- and screen-scale dependent constant)
These are fundamentally different ways to think about scrolling: One is based on rows, the other one based on pixels. They are especially different when you scroll content with varying item heights. In a row-based scroller, scrolling would feel like it "speeds up" on tall rows, whereas a pixel-based scroller wouldn't do that.
Reading the docs, Microsft thinks the interpretation should be the former. However, modern apps don't seem to be doing that: Firefox/Edge/Settings have all switched to pixel-based scrolling.
macOS long time ago defined pixel-based scrolling. I believe, GTK is in the same camp.
So I think we can safely assume that the future is pixel-based scrolling. Row-based scrolling just doesn't make much sense if you have fine-grained hardware.
So I think we have a weird set of things we need to reconcile:
- Windows defined a row-based API, but is moving towards a pixel based interpretation of it. Row-based hardware is still very common though.
- macOS is pixel based
- LCL consumers (e.g. Synedit) are overwhelmingly row-based
- macOS scrolling is much too fast
I think a good solution is one that works ok with legacy (row-based) consumers, but compatibility shouldn't be the only goal here. We should also make it possible for consumers to improve, so we should try to destroy as little information as possible so that long-term the ecosystem can transition to pixel-based scrolling.
I don't think that "pixel-based" scrolling even applies.
It's a mouse-wheel scrolling, and it would be up to an application (or a system) to interpret "scroll delta" into "pixels".
Let's keep in mind that resolutions might be different. DPI might be different.
So I don't see how "mouse wheel scrolling" can be directly mapped to "pixels".
Windows did settle this question the following manner:
any "wheel" rotation is 120. If your application/control is "lines" based, then use the number of SPI_GETWHEELSCROLLLINES to scroll up or down, every time you accumulate 120.
However many apps are not "lines based", and have to "invent" their own way of mapping (120) wheel delta to pixels.
Putting a strategic WriteLn into function TLCLCommonCallback.scrollWheel for cocoa confirms the pixel interpretation - event.scrollingDeltaY is +/- 12 or 13 for a single notch on my mouse - note the two different values. However the event.deltaY is a more sensible +/-1 , so my specific suggestion would be to use the event.deltaY as the line based assumption is too baked into the rest of the LCL.
To deliver pixel based information I think we would need to introduce a different call back/event handler.
" It's a mouse-wheel scrolling, and it would be up to an application (or a system) to interpret "scroll delta" into "pixels"."
Well, the same event is used for trackpad scroll gestures, so it's not necessarily just mouse-wheel.
And the way I understand macOS, the incoming deltas are definitely not lines: No matter if an app has large lines, small lines or no lines at all (e.g. a picture). The expectation is that the perceived scrolling amount on screen is independent of the row height.
@mftq75 "my specific suggestion would be to use the event.deltaY as the line based assumption is too baked into the rest of the LCL"
I see two problems with that suggestion:
- It would seriously break trackpad scrolling (and fine mouse wheel scrolling) on macOS. And good trackpad scrolling is something that mac users care about a lot - LCL apps are very behind on this
- Consumers already have to deal with fine grained events on Windows, where 120 is not guaranteed. And on Windows there is no way to limit events to -120/+120.
My suggestion would be to do the following:
1. Identify the most important consumers that implement custom scrolling (e.g. Synedit, and others?)
2. Update TLCLCommonCallback.scrollWheel with a patch similar to what we discussed previously. It should have two goals:
- Be scaled in such a way that the events are similar to Windows, so that controls can use the same scrolling code on all platforms
- Not destroy any information that the OS provides
3. Long term: Update controls (like Synedit) to really take advantage of the new precision
This would have a few benefits:
- It brings consistency to the events being sent out on macOS and Windows
- It would make existing controls (like Synedit) actually usable on macOS (which they currently barely are)
- It provides a path for control developers to actually take advantage of pixel-based scrolling, if they so chose. This will make the experience better on all platforms.
The downside of this approach is that we have to use a somewhat arbitrary "scaling factor". But to me that seems better than the alternative of introducing a new API, as that would bring its own incompatibilities. Also, I don't think such an API could be implemented on Windows anyway (fine vs coarse can't be distinguished there), making such an API impossible to implement in the same way on all platforms.
I have identified an additional problem with the current implementation. The line:
Msg.WheelDelta := Round(dy * LCLStep)
generates range check errors if I spin my scroll wheel fast. The overflow is genuine I think, as WheelDelta is only a smallint.
Given the current implementation is not working on Mac I stick with my assertion that the routine needs to be scaled so that 120 is one line; multiples of 120 may not be guaranteed on windows, but a value of 120 corresponds always corresponds to one line. While it would be good to allow more fine grained scrolling, I don't see any way of doing this in a way that is compatible with current code.
|2020-12-11 21:08||Daniel||New Issue|
|2020-12-11 21:08||Daniel||File Added: LazarusScrollingRepro.zip|
|2020-12-11 23:23||Bart Broersma||Category||FCL => Widgetset|
|2020-12-11 23:23||Bart Broersma||LazTarget||=> -|
|2020-12-11 23:23||Bart Broersma||Widgetset||Cocoa => Cocoa|
|2020-12-11 23:23||Bart Broersma||Project||Packages => Lazarus|
|2020-12-23 22:05||Daniel||Note Added: 0127784|
|2020-12-27 08:01||Dmitry Boyarintsev||Assigned To||=> Dmitry Boyarintsev|
|2020-12-27 08:01||Dmitry Boyarintsev||Status||new => assigned|
|2020-12-27 08:23||Dmitry Boyarintsev||Note Added: 0127811|
|2020-12-27 08:23||Dmitry Boyarintsev||Status||assigned => feedback|
|2020-12-27 08:40||Dmitry Boyarintsev||Note Added: 0127812|
|2020-12-28 20:05||Daniel||Note Added: 0127872|
|2020-12-28 20:05||Daniel||Status||feedback => assigned|
|2020-12-28 22:01||jamie philbrook||Note Added: 0127873|
|2020-12-28 22:14||Dmitry Boyarintsev||Note Added: 0127874|
|2020-12-28 23:51||jamie philbrook||Note Added: 0127876|
|2020-12-29 00:55||Dmitry Boyarintsev||Note Added: 0127879|
|2020-12-29 00:55||Dmitry Boyarintsev||File Added: wheeldelta.zip|
|2020-12-29 01:00||Dmitry Boyarintsev||Note Edited: 0127879||View Revisions|
|2020-12-29 01:25||jamie philbrook||Note Added: 0127880|
|2020-12-29 01:40||jamie philbrook||Note Edited: 0127880||View Revisions|
|2020-12-29 02:10||Dmitry Boyarintsev||Note Added: 0127881|
|2020-12-29 02:11||Dmitry Boyarintsev||Note Edited: 0127881||View Revisions|
|2020-12-29 02:28||jamie philbrook||Note Added: 0127882|
|2020-12-29 02:40||Dmitry Boyarintsev||Note Added: 0127883|
|2020-12-29 02:47||Dmitry Boyarintsev||Note Added: 0127884|
|2020-12-29 02:47||Dmitry Boyarintsev||Note Edited: 0127884||View Revisions|
|2020-12-29 02:47||Dmitry Boyarintsev||Status||assigned => feedback|
|2020-12-29 02:48||Dmitry Boyarintsev||Note Edited: 0127884||View Revisions|
|2020-12-29 02:55||jamie philbrook||Note Added: 0127885|
|2020-12-29 04:52||Dmitry Boyarintsev||Note Added: 0127888|
|2020-12-31 03:22||Daniel||Note Added: 0127958|
|2020-12-31 03:22||Daniel||Status||feedback => assigned|
|2020-12-31 04:15||Daniel||Note Added: 0127959|
|2020-12-31 05:03||Dmitry Boyarintsev||Note Added: 0127960|
|2020-12-31 07:11||Daniel||Note Added: 0127962|
|2020-12-31 07:13||Daniel||Note Edited: 0127962||View Revisions|
|2020-12-31 07:27||Dmitry Boyarintsev||Note Added: 0127963|
|2021-01-01 23:59||Daniel||Note Added: 0128011|
|2021-01-04 16:55||C Western||Note Added: 0128074|
|2021-01-04 19:15||Dmitry Boyarintsev||Note Added: 0128080|
|2021-01-04 19:15||Dmitry Boyarintsev||File Added: wheelsettings.png|
|2021-01-04 19:18||Dmitry Boyarintsev||Note Added: 0128081|
|2021-01-04 22:05||C Western||Note Added: 0128083|
|2021-01-04 22:32||Dmitry Boyarintsev||Note Added: 0128085|
|2021-01-05 19:51||Daniel||Note Added: 0128099|
|2021-01-05 21:12||Dmitry Boyarintsev||Note Added: 0128101|
|2021-01-05 22:52||C Western||Note Added: 0128105|
|2021-01-05 23:02||Daniel||Note Added: 0128107|
|2021-01-06 19:07||Daniel||Note Added: 0128129|
|2021-01-06 19:27||Daniel||Note Added: 0128130|
|2021-02-09 23:58||C Western||Note Added: 0128847|