unoplatform / uno

Open-source platform for building cross-platform native Mobile, Web, Desktop and Embedded apps quickly. Create rich, C#/XAML, single-codebase apps from any IDE. Hot Reload included! 90m+ NuGet Downloads!!
https://platform.uno
Apache License 2.0
8.97k stars 729 forks source link

`Button` events are improperly raised/not raised for the three `ClickMode` modes on various platforms #7888

Open mikebmcl opened 2 years ago

mikebmcl commented 2 years ago

Current behavior

Testing on WASM, Android, and Skia-Gtk revealed that button events don't match UWP's behavior and that on UWP the events that should be raised depend on which of the ClickMode values a button is set to have. For WASM I tested using MS Edge, Chrome, and Firefox and found that the behaviors vary on Edge/Chrome versus Firefox. I'll post my detailed test results as a comment to this report because it's rather long.

Expected behavior

Button events will be raised at the same times and under the same circumstances as they are on UWP.

How to reproduce it (as minimally and precisely as possible)

Run the SamplesApp Buttons->Button_Events sample, changing between the three ClickMode values on the various platforms. The default ClickMode is ClickMode.Release so that's what the sample currently tests (there are differences from UWP even on that mode but most only show up for touch input and the ones that do show up for mouse are easy to miss).

To ease that testing I created three SamplesApp tests that use Button_Events as a starting point, adding one test for each of the three ClickMode values (Button_Events_Hover, Button_Events_Press, and Button_Events_Release) with a modified UI so that it could be tested on Android (and other small form-factor devices). I'm going to submit them as a test: PR in hopes that they will be useful.

Workaround

None.

Works on UWP/WinUI

No response

Environment

Uno.UI / Uno.UI.WebAssembly / Uno.UI.Skia

NuGet package version(s)

The current main branch of the uno repo except for skia which I tested using the /dev/dr/runtimeUITests branch because it resolves some Skia issues that could've affected testing.

Affected platforms

Android, WebAssembly, Skia (GTK on Linux/macOS/Windows)

IDE

Visual Studio 2022

IDE version

17.0.5

Relevant plugins

No response

Anything else we need to know?

I'm unable to test this on iOS, macOS, Skia/Tizen, skipped testing on Skia/WPF, and only tested WASM on Edge, Chrome, and Firefox. It's likely that those other platforms and that other browsers with WASM will have their own event behavior differences. I skipped testing on Skia/WPF because the issues I found with Skia/Gtk revealed problems that made testing further on Skia revealed two bugs that made further testing unlikely to be of any value before they are fixed.

mikebmcl commented 2 years ago

Results from running the tests using the tests in #7892 on WASM (Edge/Chrome and Firefox), Android, and Skia/Gtk. (Note: I originally started testing this using the existing Button_Events SamplesApp test but realized that creating individual tests for each of the three ClickMode types with small form-factor friendly UI made testing much easier. The tests can be run using Button_Events on WASM, Skia, etc. and will produce the same behaviors except that it does not test the PointerCaptureLost event so you'd need to add a test for that to Button_Events to test that event.)

WASM (Windows 11 Pro Version 10.0.22000 Build 22000) (Edge 97.0.1072.62 (Official build) (64-bit)) (Firefox 96.0.2 (64-bit)) (Chrome 97.0.4692.99 (Official build) (64-bit))

Note: Unless otherwise noted, the problems below occur on all tested browsers.

Firefox: Touch: All buttons in all Events tests: Visual state when starting a touch inside a button and then moving outside does not change until the button is released. Firefox using a mouse, UWP, Chrome, and Edge all change visual state as soon as the pointer leaves the button.

Hover Mouse:

Hover Touch:

Press Mouse:

Press Touch:

Release Mouse:

Release Touch:

====================================

Android Emulator API 28 (Touch only)

Hover:

Press:

Release:

==========================

Skia/Gtk Windows using /dev/dr/runtimeUITests

UPDATE: I tested this against the current Skia/Gtk Windows and the behavior is very different than the behavior when using /dev/dr/runtimeUITests, which is PR #7845 . A brief check showed that the touch visual state getting stuck in pressed seems to rarely occur with the current code. There are some other issues that exist with the current code that don't exist with the #7845 changes. I'll do a full test of each later today and report the findings in a new comment here. #7845 shouldn't be blocked by this since it fixes #7811 (scrollbars and scrollviewer not working). Just noting that it changes other behavior such as buttons (mouse and touch). I'll add a comment on #7845 if my tests show that it broke something important as far as mouse input goes.

Hover Mouse:

Hover Touch:

Press Mouse:

Resolving the incorrect raising of events based on entering/exiting internal elements of a button in Skia and its failure to change visual states correctly for touch input will likely resolve many of the behavior and conformance issues so I'm stopping at the Press Mouse Pointer Exited test for now since further testing will just continue producing those issues (possibly masking other issues in the process). (Note: As stated above, I'm going to do full tests of Skia/Gtk using the current code and the branch in PR #7845 to see what changed between the two. I will likely also do brief testing of Skia/WPF to see if there are any differences between it and Skia/Gtk).

jeromelaban commented 2 years ago

/cc @dr1rrb

mikebmcl commented 2 years ago

This took longer than I thought it would, but I finished testing Skia/Gtk Windows (native not WSL) for current versus the incoming changes in #7845 . I mention it at the end of the notes but since it's rather important I wanted to mention it here too.

I did not do comprehensive testing of Skia/Gtk WSL or Skia/Wpf but I did test them for two specific events where the Skia/Gtk Windows behavior ranged from slightly wrong to outright bizarre and it turns out that they both are very close to conforming to UWP's behavior.

They both have the same problem that I mentioned in a comment on #7845 and both seem to exhibit the same visual state change issues I observed with Skia/Gtk Windows, but my guess is that most or all of the touch input issues that appeared in Skia/Gtk Windows don't exist for Wpf and for Gtk WSL (Wayland-based so I don't know if Gtk on X11 would behave like WSL or like the Gtk Windows).

Also, I discovered that UWP's behavior for various events differs sometimes if there's a mouse right-click rather than a left-click. This wouldn't be notable except for the fact that there are events that are not raised for left-click but are raised on a right-click. So it's not simply a case of buttons ignoring right click pointer events. I only discovered this when testing against #7845 and did not go back and test against the existing code. So the notes for right click behavior are only in the second section of results.

=========

Skia/Gtk Windows 11 Pro Version 10.0.22000 Build 22000 using uno master as of commit 4a63fa2

Note: Any mention below of the "yellow rectangle" or "yellow rectangle inside the button" or anything similar means the portion of the IsHitTestVisible="True" Border (which has a Gold SolidColorBrush as its Background) that is inside the Button being used for the test, including the part that overlaps the IsHitTestVisible="False" Border (which has a BlueViolet SolidColorBrush Background). If a "purple rectangle" or something similar is mentioned, then it means the portion of that BlueViolet Border that is inside the button that does not overlap the "yellow rectangle". Any portion of either of those borders that is outside the button should be considered as behaving the same as everything else outside the border; if there is some reason that it does behave differently then that will be explicitly noted in a way that distinguishes it from the part that is inside the button. Also, any mention of starting, ending, entering, or exiting the button always means a portion of the button that is not within the "yellow rectangle". Button behavior that occurs inside the "purple rectangle" was generally found to be identical to behavior in other, non-"yellow rectangle" parts of the button. Because of that, most test results below were not tested specifically for purple rectangle behavior unless otherwise noted.

Note: In the tests, when a button becomes "stuck" in a pressed visual state, I sometimes tested to see if tabbing to other controls would reset it. I did not test this consistently but every time I tested it, tabbing did not reset the button back to the unpressed visual state that it should have been in. Sometimes I mention that I tried tabbing. The absence of any note about tabbing should not be taken to mean that I tried tabbing and it reset the control to an unpressed state.

Hover Mouse:

General note: At times moving the pointer into the button does not trigger a visual state change to pressed. This only seems to occur when the mouse is moved rapidly into the button's bounds. When this occurs, moving it out may trigger a visual state change to pressed but if so it will then change to unpressed; it will not become stuck in pressed state.

General note: At times moving the pointer out of the button when it is in pressed visual state will not trigger a visual state change to unpressed. This only seems to occur when the mouse is moved rapidly out of the button's bounds. When this happens it will remain in the pressed state until moving the pointer back into the button then moving it slowly out again. The exact speed where it does and does not occur can make it seem arbitrary if the movement speed is near that speed that causes it to become stuck in a pressed visual state. If stuck, it will not switch back to unpressed by clicking outside of the button no matter how many times you click. It will not switch if you move the pointer over another button or click on the hamburger button to get the list of tests to show. It will not switch if you minimize the window then restore it. It will not switch if you maximize then restore. It will not switch if you use touch input outside of the button. It will not switch if you tab through the various controls including tabbing to the button that is stuck and then tab away from it to another control. It will not switch if you change to a different window then change back.

General note: If the pointer moves into the button's bound inside the yellow rectangle that is hit test visible (and thus should block notifying the rectangle) it will correctly block notifications even if the becomes stuck in a visual state. However after moving the pointer into the rectangle somewhere other than in the yellow rectangle, the visual state will remain the same no matter how many times you cross the yellow rectangle or at what speed. Additionally it will not switch states when moving out of the rectangle within the bounds of the yellow rectangle no matter what speed you move the pointer. Basically, it obeys hit testing if it enters the yellow rectangle first but ignores it for the yellow rectangle if it enters the button first.

General note: A note will be added for a specific test if that test does not exhibit the above visual state issues; in the absence of a note assume that the test exhibited the issues. Unless otherwise noted, these issues do not seem to impact the behavior of the test. For example, tapped works correctly regardless of whether or not it gets stuck in the pressed visual state or fails to switch to the pressed visual state.

Hover Touch:

General note: The button will get stuck in pressed state sometimes, seemingly when a touch begins outside the button, is moved into the button, then is released while still in the button. Tapping and releasing the button does not switch back to not pressed nor does tapping outside the button. Tapping outside the button, moving in, then moving out and releasing will return the button to not pressed. It will not change to not pressed if the tap begins inside the button then moves out however if the tap begins inside the button, is dragged over the yellow rectangle inside the button, and is then moved out of the button it will switch to not pressed.

General note: The navigation buttons need to be pressed twice when using touch input. The first touch switches them to the pressed state but does not result in the bound command being executed. The second touch switches back to unpressed and the command is executed. The navigation buttons are in the default ClickMode, ClickMode.Release.

Press Mouse:

General note: The sticking of visual state behavior for hover mouse does not seem to be the same for press mouse. Notes will be made in each test if there is any visual state problem.

Press Touch:

General note: If a tap happens it will put the button into the pressed visual state and keep it there. Tapping again anywhere will switch the button back to unpressed except if it is a tap outside the button that is moved into the button before being released (in which case it stays in the pressed visual state).

Release Mouse:

Release Touch:

General note: For all of the touch tests above, pressing and holding for 2+ seconds brings up what appears to be an empty context menu. Where events do not occur that should have, this may be the reason. It is also possible that this stops events from being raised that otherwise would have been improperly raised.

================================================

Skia/Gtk Windows 11 Pro Version 10.0.22000 Build 22000 using branch /dev/dr/runtimeUITests as of commit 139cdda9

General Note: Unless otherwise stated, all events that are invoked due to movement into or out of a control suffer from an issue where whenever the pointer moves out of a part of the button into its container (e.g. from the Content to the ContentPresenter), the event is raised. Also, the first time the pointer moves into a part of the button the event will (correctly) not be raised but after that each time it enters that part the event will be raised (in addition to being raised whenever it exits).

This only affect movement within the parts of the button. Movement out of the button or into the button only invokes the event if it is supposed to be invoked. This effects Click for ClickMode.Hover and PointerEntered and PointerExited for all ClickMode enumerators. The one exception is that for PointerEntered and PointerExited, if the movement into or out of the button is very fast, then the event will only be raised at the time it should be raised. In the case of touch input that begins inside a button, if movement before releasing the touch is intended then the touch must be rapidly moved out of the button immediately after it begins to avoid the chance of the event raising due to movements between parts of the button.

If the events that are susceptible this conform to UWP behavior in all other regards then no note will be made about the event specifically, e.g. for ClickMode.Hover with Mouse input, Click, PointerEntered, and PointerExited all behave identically to UWP except for this issue and so no mention of them is made below.

General Note: I did not test right-click behavior for the previous set of tests but upon discovering that right click was raised during one of these tests when a left click did not raise the event I decided to test right clicks here. I will only report when the Skia behavior differs from UWP or when right click behaves differently than left click. If a behavior difference is noted without any mention of platform, then the behavior difference was the same on UWP and Skia.

Hover Mouse:

General note: The same touch and hold for 2+ seconds bringing up an empty context menu behavior occurs with this branch. This applies to all touch tests (Hover, Press, and Release).

General note: Unlike with previous event tests, most of these touch tests are split up into multiple paragraphs because they have a number of behaviors that vary depending on multiple factors, e.g., there are places where the behavior is different for a touch that begins outside the button, moves into the button, then moves into the yellow rectangle inside the button versus a touch that begins outside the button, moves into the button, then moves outside of the button without going into the yellow rectangle inside the button. In general, if the behavior of entering the yellow rectangle inside the button and exiting the button are the same, it will just be noted as exiting the rectangle. The same for entering. I can't guarantee that I remembered to test entering/exiting the yellow rectangle for every specific case. So there may be a few cases where there is different behavior for the yellow rectangle and outside the button but the difference isn't noted because I forgot to test it or thought I did but made a mistake. This applies to all touch tests (Hover, Press, and Release).

Hover Touch:

If the first touch is not rapid, then the first tap puts the button into pressed state. A second tap following does not raise double tapped and keeps the pressed state. A tap after that raises double tapped regardless of how long a delay there is and keeps the pressed state. From there on, every second tap will raise double tapped regardless of delay and visual state remains pressed.

Tapping outside the button sets its state back to unpressed and reset the button's behavior to its initial state. However if the tap outside is in the yellow rectangle within the button then the button visual state will switch back to unpressed however the next touch inside the button might raise the event, presumably depending on the internal state that keeps track of double tap.

If the button is in a pressed visual state and a tap happens somewhere in the button other than where the previous tap occurred, the button will remain in the pressed visual state but will not raise the event but instead will begin a new cycle with that as the point that must be pressed to get double click events to raise.

Whenever the event is raised, it is always raised on release rather than press.

If a tap is held then it will not raise the event, will keep the pressed visual state, and the button will not raise the event until the third quick tap after the tap that was held.

It is raised when exiting the button if it begins inside the button and is moved out. The visual state will switch to pressed and then back to unpressed at the moment that the touch exits the button.

If a touch is released inside the button, leaving it in the pressed visual state, then subsequent taps on the button without any subtantial movement do not raise click, even if the subsequent tap is in a different part of the button, unless the touch point is right at the edge of the button but still within it. It is possible that this edge of the button behavior is actual due to motion being registered from the ContentPresenter into the Border since touch input is not as precise as mouse input due to the size of a person's fingertip.

When the button is stuck in the pressed visual state, tapping outside the button will reset its visual state to unpressed and the event will (correctly) not be raised unless the touch then proceeds to enter the button.

A touch that starts inside the button and moves into the yellow rectangle inside the button or starts outside and moves rapidly through the button into the yellow rectangle raises the event upon crossing into the yellow rectangle rather than when the touch begins/enters the button and the button will change to the pressed state then back to unpressed when it crosses into the yellow rectangle.

It is properly raised when no significant movement occurs but leaves the button in the pressed visual state. Subsequent taps will continue to properly raise the event but the button will remain in the pressed state.

It is not raised when the touch starts outside the button and moves in before being released. The button will be left in the pressed visual state.

A rapid movement into the button from a touch that begins outside of it will (correctly) not raise the event but if the touch is then released without any substantial movement the event will not be raised.

Press Mouse:

It is raised no matter how much time passes between the first tap and the press that follows.

Press Touch:

A first tap puts the button into hover visual state. The next tap will put it into pressed state but will not raise the event. The tap after that (i.e. the third tap) will raise the event and the button will remain in pressed visual state. From this point every other tap will raise the event and the visual state will switch between hover and pressed on each tap.

A first tap puts the button into hover visual state. The next tap will put it into pressed state but will not raise the event. A tap outside the button will put it back into unpressed visual state. But if the button is then tapped at approximately the same place as the previous two taps on the button the event will be raised. If the tap is in a different location within the button then it will not be raised. Subsequent taps outside then inside the button will sometimes result in the event being raised when tapping in the button, but I'm unable to find a pattern that reliably triggers this so I am simply noting that sometimes the event will be raised when tapping inside the button even though the previous tap was outside the button. This behavior can also occur with the previous consistent taps within the button if a tap is made outside the button at some point.

If the first tap is held, the button will be put into the hover state. Subsequent long taps in the button will not have any discernable effect but short taps will raise the event at the times described in the first test as if the subsequent long taps never occurred. The exception is that after the short tap that raises the event occurs, a long tap will switch the button back to hover state with the same subsequent behavior as if it was a short tap rather than a long tap.

If the first touch begins outside the button, is moved into the button and released, the button will switch to hover visual state and will behave the same as if the the first touch was a tap within the button.

A first tap inside the button followed by a second tap inside the button (the same as the first test) followed by a tap and hold that is then moved outside of the button before release will not raise the event but if the next tap is in the button at approximately the same location as the previous touches, the event will be raised.

This is not a comprehensive list of all behaviors since all permutations of short and long touches that do or do not involve movement into or out of the button would take a very long time and would probably not provide additional value beyond the behavior already documented.

The event will be raised if the touch begins in the button and is moved out of the button without holding it long enough to bring up the context menu. This happens no matter what state the button is in before doing it. Each time this is done the event will be raised such that doing this multiple times in a row will raise the event every time. If the touch is held long enough to bring up the context menu then the event will not be raised if the touch is then moved out of the button.

If the button is in the unpressed state then the event will be raised when the button is tapped, including if the touch is held before releasing, but the event is not raised for subsequent taps within the button but short taps will switch the visual state between hover and pressed.

It is not raised when a touch begins within a button and is released.

Release Mouse:

It is raised no matter how much time passes between the first tap and the press that follows.

It is not raised on right clicking.

Release Touch:

If the button is in the unpressed state then the event will be raised when the button is tapped, including if the touch is held before releasing, but the event is not raised for subsequent taps within the button but short taps will switch the visual state between hover and pressed.

It is not raised when a touch begins within a button and is released.

==================

Important: Skia/Gtk running on WSL and Skia/Wpf exhibit at least some behavioral differences from Skia/Gtk Windows. They still exhibit the raising Enter/Exit events whenever moving over internal parts of the control behavior. As an example of different behaviors:

Skia/Gtk WSL and Skia/Wpf Hover Touch properly raises the Tapped event on the first touch and otherwise conforms to UWP behavior except that the first touch puts it into the pressed visual state and it remains there on subsequent taps.

Skia/Gtk WSL and Skia/Wpf Press Touch Double Tapped does not raise until the release of the second tap but otherwise seem to conform to UWP. Which is rather nice to discover given how strange Skia/Gtk Windows behaves.

dr1rrb commented 2 years ago

Hi @mikebmcl , really impressive investigation. Some of those are somehow known (https://github.com/unoplatform/uno/blob/master/doc/articles/features/pointers-keyboard-and-other-user-inputs.md#known-limitations-for-pointer-events - Some was fixed, docs needs to be updated), and fixing all of them at once would require a massive work.

For instance enter/exit are known to be problematic on Android and iOS which are doing what I call "implicit capture" (once pressed, a pointer is always routed to the same element, not mater if pointer was move above another element, even if not captured by the app). To fix those it would require that we do the pointer dispatch in managed code, and go in native only when we reach a non managed element.

So for now we are only trying to get as close as possible to UWP behavior.

I've look into test that you are adding in https://github.com/unoplatform/uno/pull/7892 and I noticed 2 majors issues:

So even if I would love to be able to fix all issues you mentioned above in order to be 100% compliant with UWP behavior, for now I would recommend that we focus on issues that are blocking (or even just annoying) in projects. Slowly we are getting even closer :)

GitHub
uno/pointers-keyboard-and-other-user-inputs.md at master · unoplatform/uno
Build Mobile, Desktop and WebAssembly apps with C# and XAML. Today. Open source and professionally supported. - uno/pointers-keyboard-and-other-user-inputs.md at master · unoplatform/uno