Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PointerExited event not always fired (when pointer captured) #1959

Open
Tracked by #12037 ...
benstevens48 opened this issue Feb 9, 2020 · 13 comments
Open
Tracked by #12037 ...

PointerExited event not always fired (when pointer captured) #1959

benstevens48 opened this issue Feb 9, 2020 · 13 comments
Labels
area-Mouse bug Something isn't working needs-winui-3 Indicates that feature can only be done in WinUI 3.0 or beyond. (needs winui 3) team-CompInput Issue for IXP (Composition, Input) team

Comments

@benstevens48
Copy link

If I mouse over a control (so PointerEntered is fired), press, then drag outside the control, the PointerExited event does not fire in at least some cases. I didn't spend a long time testing but it definitely occurs when ManipulationStarted is triggered (and maybe even if not - I didn't test it). It would be useful if we could guarantee the PointerExited event fires. This is important because, for example, we might change the cursor or control style when the pointer is over a specific elements etc, and without this event firing the change is permanent. (It would also be useful if controls had IsPointerOver and IsPointerPressed properties, but that's another issue). The PointerExited event should probably fire when the pointer is released outside the control and also if the control is unloaded before the pointer leaves the control. I assume all built-in controls use the PointerEntered/Exited events for transitioning to/from the PointerOver visual state, so this bug potentially effects that as well.

Also, it would be nice if PointerReleased was always called after PointerPressed, rather than having to listen to PointerReleased, PointerCanceled and PointerCaptureLost (I think all 3 are necessary currently), but maybe that's necessary due to the design of pointer capture and manipulation.

I've put together the following workaround which provides the desired behaviour (if you listen to its IsPointerOverChanged event), but it would be good if I didn't need this workaround - https://github.com/Pictureflect/pictureflect-parital-source/blob/master/PictureflectPartialSource/PointerOverManager.cs.

@msft-github-bot msft-github-bot added the needs-triage Issue needs to be triaged by the area owners label Feb 9, 2020
@robloo
Copy link
Contributor

robloo commented Feb 12, 2020

PointerExited should always fire if PointerEntered did, if it doesn't that's a bug.

Starting with Windows 8.1, PointerExited is fired for any case where the pointer had at one time fired a PointerEntered event, but some UI state change happens where the pointer is no longer within that element. This includes cases where the whole element disappears. And if the pointer is now over a different element because a previous element disappeared, that element fires PointerEntered, even if the pointer never moves. Elements that set their Visibility to Collapsed programmatically is one way that elements might disappear from UI, and the Windows 8.1 behavior accounts for this and will fire PointerExited for the Collapsed element and PointerEntered for the newly revealed element.

https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.pointerexited

@chrisglein
Copy link
Member

@benstevens48 definitely a bug if these events aren't paired. Can you give more detail on your specific configuration for this? I'm unable to reproduce this so far.

To reproduce I tried setting up something like this:

    <StackPanel Padding="100" HorizontalAlignment="Center">
        <Button Height="200" Width="200" PointerEntered="IncrementPointerEntered" PointerExited="IncrementPointerExited"/>
        <TextBlock x:Name="EnteredCount"></TextBlock>
        <TextBlock x:Name="ExitedCount"></TextBlock>
    </StackPanel>
        private int _entered = 0;
        private int _exited = 0;

        private void IncrementPointerEntered(object sender, PointerRoutedEventArgs e)
        {
            _entered++;
            EnteredCount.Text = "PointerEntered: " + _entered.ToString();
        }

        private void IncrementPointerExited(object sender, PointerRoutedEventArgs e)
        {
            _exited++;
            ExitedCount.Text = "PointerExited: " + _exited.ToString();
        }

image

I can never get the event counts to drift apart. Can you clarify what more is essential for the repro?

@chrisglein chrisglein added needs-author-feedback Asked author to supply more information. and removed needs-triage Issue needs to be triaged by the area owners labels Feb 28, 2020
@benstevens48
Copy link
Author

@chrisglein - it actually seems to be related to pointer capture. Try this instead.

<Grid Height="200" Width="200" Background="Beige" PointerEntered="IncrementPointerEntered" PointerExited="IncrementPointerExited">
                <Border Width="100" Height="100" Background="Red" PointerPressed="Border_PointerPressed" PointerReleased="Border_PointerReleased"></Border>
            </Grid>
        private int _entered = 0;
        private int _exited = 0;

        private void IncrementPointerEntered(object sender, PointerRoutedEventArgs e) {
            _entered++;
            EnteredCount.Text = "PointerEntered: " + _entered.ToString();
        }

        private void IncrementPointerExited(object sender, PointerRoutedEventArgs e) {
            _exited++;
            ExitedCount.Text = "PointerExited: " + _exited.ToString();
        }

        private void Border_PointerPressed(object sender, PointerRoutedEventArgs e) {
            System.Diagnostics.Debug.WriteLine("pressed");
            var button = sender as UIElement;
            button.CapturePointer(e.Pointer);
        }

        private void Border_PointerReleased(object sender, PointerRoutedEventArgs e) {
            System.Diagnostics.Debug.WriteLine("released");
            var button = sender as UIElement;
            button.ReleasePointerCapture(e.Pointer);
        }

Click on the red square then drag outside the beige container and release. This is presumably due to the way pointer capture works. In my opinion the PointerExited event still needs to fire at some point though. Either when the pointer leaves the grid or when the pointer capture is released outside the grid (but in that case it would differ from the behavior of adding all the events to the containing grid itself so I'm not sure which is best - possibly you could change the behavior of PointerExited not to fire until pointer capture is lost even on the element that has pointer capture, because in a sense the pointer is still inside the element until then).

@msft-github-bot msft-github-bot added needs-triage Issue needs to be triaged by the area owners and removed needs-author-feedback Asked author to supply more information. labels Feb 28, 2020
@chrisglein
Copy link
Member

Gotcha. Thanks for the details on that @benstevens48, much appreciated!
Will require WinUI3 to complete (this code is not in WinUI2), tagging as such.

@chrisglein chrisglein added area-Mouse needs-winui-3 Indicates that feature can only be done in WinUI 3.0 or beyond. (needs winui 3) and removed needs-triage Issue needs to be triaged by the area owners labels Feb 29, 2020
@chrisglein chrisglein changed the title PointerExited event not always fired PointerExited event not always fired (when pointer captured) Feb 29, 2020
@benstevens48
Copy link
Author

@chrisglein - it turns out there are other scenarios when the PointerExited event does not fire. I have discovered this while trying to implement a drag and drop. Suppose we have a FrameworkElement A that undergoes the follow events: PointerEntered, PointerPressed, pointer capture is taken by some other FrameworkElement B outside element A, then the pointer is moved outside element A, then the pointer is released. The pointer exited event for element A is never fired. it should probably be fired at the point pointer capture is taken by element B.

I would also like to confirm that in the previous scenario where a sub-element captures the pointer, it would make most sense for PointerExited not to fire when the pointer moves outside the sub-element if the element has pointer capture. The PointerExited event for the sub-element should fire on pointer capture lost instead (if the pointer is not over the sub-element at that point), This it because PointerMoved events still fire for the sub-element (which is essential for e.g. dragging), and one would expect PointerExited to fire after any PointerMoved events.

Finally, I would like to mention that there are serious problems with the built-in drag and drop functionality regarding events like pointer released etc. It seems that at the time when dragstarting occurs, pointer capture is taken away from the app entirely. So even if you add pointer pressed and pointer released event handlers to the core window, you will not see a pointer released event when the drag completes (and possibly this includes when the dragstarting is canceled - I can't remember). In my opinion, it ought to be possible to determine when the pointer is released. I struggled for a very long time working out the sequence of events and what is being given pointer capture (if you add CanDrag to an element then it will be given pointer capture on PointerPressed or something which will then be taken away on dragstarting), and which events fire and which events are missed out, and along with trying to deal with this bug #1540, which occurs if you set CanDrag to false between PointerPressed and DragStarting, it ended up taking my entire day and was somewhat frustrating! At the very least it would be nice if the sequence of pointer events that occur when doing drag and drop could be documented.

@benstevens48
Copy link
Author

Also, this doesn't just apply to mouse (I don't know if the 'area-Mouse' label added to this issue includes touch as well), because the PointerEntered and Exited events will fire for touch too (when in contact).

@Felix-Dev
Copy link
Contributor

Felix-Dev commented Apr 19, 2020

@benstevens48

Suppose we have a FrameworkElement A that undergoes the follow events: PointerEntered, PointerPressed, pointer capture is taken by some other FrameworkElement B outside element A, then the pointer is moved outside element A, then the pointer is released. The pointer exited event for element A is never fired.

This behavior is called out in the docs:

If another element has captured the pointer, PointerExited won't fire even if the captured pointer leaves an element's bounds. For more info on pointer capture, see CapturePointer or Mouse interactions.

@benstevens48
Copy link
Author

@Felix-Dev OK, but we need an event that is guaranteed to fire after pointer entered else how are we supposed to do things like set a different cursor or visual state on hover? If pointer exited never fires then the cursor or visual state will be changed permanently.

The quote you mentioned seems to be in contradiction to the following quote lower down the page.

Starting with Windows 8.1, PointerExited is fired for any case where the pointer had at one time fired a PointerEntered event, but some UI state change happens where the pointer is no longer within that element. This includes cases where the whole element disappears. And if the pointer is now over a different element because a previous element disappeared, that element fires PointerEntered, even if the pointer never moves. Elements that set their Visibility to Collapsed programmatically is one way that elements might disappear from UI, and the Windows 8.1 behavior accounts for this and will fire PointerExited for the Collapsed element and PointerEntered for the newly revealed element.

@Felix-Dev
Copy link
Contributor

Felix-Dev commented Apr 19, 2020

I don't think there is a contradiction here:

Starting with Windows 8.1, PointerExited is fired for any case where the pointer had at one time fired a PointerEntered event, but some UI state change happens where the pointer is no longer within that element.

The case mentioned in the docs above is about another element capturing the pointer, and not one of the UI state changes listed in the paragraph you are quoting.

@benstevens48
Copy link
Author

Ok, well regardless of what it says in the docs there needs to be a reliable way of changing visual state and cursor on hover without the change being permanent.

@edward-a
Copy link

edward-a commented Sep 1, 2020

Is this going to be fixed in WinUI 3? This is a serious flaw regardless what docs say.

@jtorjo
Copy link

jtorjo commented Feb 17, 2021

I can confirm this happens (PointerExited event not fired), even when the mouse is NOT captured.
I have a grid on the left and a grid on the right.

image

I capture PointerExited on the left. It's sent probably 5% of the time. The workaround for me is to capture PointerEntered for the right pane. It's quite sad.

LATER EDIT: Let me be clear: nor PointerCancelled, nor PointerCaptureLost events are raised

@benstevens48
Copy link
Author

Need to check this has been fixed in WinUI3. Would also be good if it was fixed in system XAML.

@bpulliam bpulliam added team-CompInput Issue for IXP (Composition, Input) team and removed team-Framework labels Aug 22, 2023
@microsoft-github-policy-service microsoft-github-policy-service bot added the needs-triage Issue needs to be triaged by the area owners label Aug 22, 2023
@bpulliam bpulliam removed the needs-triage Issue needs to be triaged by the area owners label Aug 29, 2023
@duncanmacmichael duncanmacmichael added the bug Something isn't working label Nov 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Mouse bug Something isn't working needs-winui-3 Indicates that feature can only be done in WinUI 3.0 or beyond. (needs winui 3) team-CompInput Issue for IXP (Composition, Input) team
Projects
None yet
Development

No branches or pull requests

10 participants