rust-windowing / winit

Window handling library in pure Rust
https://docs.rs/winit/
Apache License 2.0
4.88k stars 913 forks source link

Click count on Pointer Events #3899

Open xorgy opened 2 months ago

xorgy commented 2 months ago

Click counting for double and triple clicks is a common need, and on some platforms is hard to address if they aren't determined in winit. In particular the Mac doesn't have a readily accessible API for getting the double click interval or detection rectangle separate from an individual event.

Some notes on implementation:

Overall, I think this would be not terribly hard to implement in a useful way; and even on platforms where there are shortcomings, it's better than not having it at all.

Blocked on #3833.

Enyium commented 2 months ago

Not to say you said that, but to clarify: I think winit's interpretation of higher-order clicks should not artificially be limited to support at most triple-clicks. It should be unbound.

Note that, on Windows, you should now use GetSystemMetricsForDpi() instead of GetSystemMetrics(). For me, GetSystemMetricsForDpi() on Windows 10 always returns 4 for SM_CXDOUBLECLK and SM_CYDOUBLECLK for the DPI values 1 to 0x7fff_ffff. But Microsoft could, of couse, change that any time.

Also, Windows treats the down-action as the double-click event, not the up-action. When the window class style CS_DBLCLKS is set, the messages sent are WM_LBUTTONDOWN, WM_LBUTTONUP, WM_LBUTTONDBLCLK, WM_LBUTTONUP (proof). CS_DBLCLKS shouldn't be set, if you're interpreting double- and higher-order clicks yourself, or you'd alternatively need to treat WM_LBUTTONDBLCLK the same as WM_LBUTTONUP (same for other buttons). (I don't know if winit wants to be resilient against unexpected style changes from other code via SetClassLongPtr(), in which case it could handle WM_LBUTTONDBLCLK [and all others ending in ...DBLCLK], even though it doesn't set CS_DBLCLKS.)

Notably, however, browsers send the dblclick DOM event on mouse-up, not on mouse-down (tested on Windows 10 with Firefox and Brave, which is Chromium, using this page).

I think, ideally, winit would adhere to the more snappy behavior of sending higher-order click events on mouse-down; at least on Windows where the OS sets this precedent.

The article "Implementing higher-order clicks" from Raymond Chen contains some valuable information, like on how SM_CXDOUBLECLK and SM_CYDOUBLECLK are to be interpreted.

Enyium commented 2 months ago

GPT-4o thinks slow drifting during the performance of a higher-order click is allowed. According to it, the spacial constraints would only apply between two consecutive clicks, and not from the first click in the series to the one currently judged.

Custom-drawing UI frameworks like Slint would need to reasonably assess whether a higher-order click (which starts with double-clicks) happened on a certain widget. In the old days, the widgets were different child windows with their own class styles and window procedures, which meant that double-clicks where one click happened outside and the next inside the widget, weren't interpreted as double-clicks onto the widget.

  1. To allow for everything to be handled perfectly, you actually seem to need to send the whole series of accumulated coordinates of the series of connected clicks to the event receiver, because widgets could have any shape (like circular).
  2. An easier way that wouldn't involve a list with a coordinate count not known from the beginning would be to combine all coordinates of a click series into a single rectangle and only send it. (Every additional click that's temporally and spacially validly connected to the click series extends the built up rectangle.)
  3. Yet another way would be to disallow drifts like mentioned at the start of the comment, and only send the coordinate of the latest click. This would then mean that the end user could theoretically have performed previous clicks outside the widget and only the last one inside it, and the UI framework would need to see this as a higher-order click onto the widget. But, with no drift allowed, this effect couldn't involve a large area.

But maybe a reset API would be appropriate, so coordinates would be checked one by one by the UI framework to identify the widget that was hit, and the framework would be responsible to reset winit's click count when the latest higher-order click event's widget isn't the same as the previous click event's widget. In this case, the UI framework would tell winit to reset the click count and treat the click event as a regular click.

kchibisov commented 2 months ago

No event should be added for that, it should be just a property of the existing button event telling how deep you're in the click sequence(or whatever you call it).

Users can figure out themselves how they want to handle double clicks, etc, we just should provide with that information, nothing more, at least for now.

Enyium commented 2 months ago

I wasn't saying another event should be added. winit just needs to provide enough information, so higher-order clicks can behave in the best way possible, which is the case when the UI framework at the end of the line can safely attribute the event to a certain widget. I'm tending towards the approach outlined in my last paragraph.

It may only be a little awkward regarding the data that events bring with them, if on some platforms, higher-order clicks happen on mouse-down, and on other platforms on mouse-up. Having the same data (like click count) in both events, and having to handle them in both wouldn't be that great. But possible, if necessary, maybe as num_clicks: Option<NonZeroUsize>, where the Option would convey whether the event type (down/up) is the one on the platform that is associated with higher-order clicks as well as whether a higher-order click (double-clicks and up) happened. Then, the UI framework must converge mouse-down and mouse-up to a higher-order handler itself.

xorgy commented 2 months ago

It's not that awkward. There is only one major environment where click count is directly provided on pointer related events (macOS). We should match the cases where it is available on macOS events, using a combination of platform settings (on Windows) and sensible defaults (on X11, Wayland, Android, etc.). It will most likely be Option<NonZero> anyway (though usize is gratuitous). It is still possible for toolkits and applications to implement their own click counting, though generally it wouldn't be any better than this. Double click behavior is rarely based on widget bounds, it is usually based on a fixed maximum distance per click from the previous click; but if somebody wanted to implement that nobody is going to stop them.

kchibisov commented 2 months ago

The alternative here is that we may just provide a data to detect double clicks and let the application developer decide how they want to process all of that. Like e.g. define a region for double click and interval, but let the actual count be on the user.

Though, given how small it is I don't think it hurt to do in winit based on macOS as suggested.

xorgy commented 2 months ago

The attraction to having it in the event from Winit for Masonry specifically is that it covers everything in our internal PointerEvent once we have click counts. The other side is that it may not be possible to implement it correctly on macOS without taking the click count from the NSEvent.