emclient / mac-playground

70 stars 10 forks source link

Mouse capture implementation question #5

Open PaulBol opened 5 years ago

PaulBol commented 5 years ago

Something else that I noticed...

UngrabWindow in XplatUICocoa.cs includes these lines

if (LastEnteredHwnd != IntPtr.Zero && LastEnteredHwnd != grabbed)
{
    var lparam = IntPtr.Zero; // TODO: should contain mouse coords? See WindowsEventResponder.TranslateMouseEvent()
    var wparam = (IntPtr)(NSEvent.CurrentModifierFlags.ToWParam() | Mac.Extensions.ButtonMaskToWParam(NSEvent.CurrentPressedMouseButtons));
    SendMessage(grabbed, Msg.WM_MOUSELEAVE, wparam, lparam);
    SendMessage(LastEnteredHwnd, Msg.WM_MOUSE_ENTER, wparam, lparam);
}

This leads to a nested call of WndProc with WM_MOUSELEAVE when the mouse button is released (WM_LBUTTONUP) while captured.

My code was not prepared to deal with this. I'm not daring to suggest this is a bug in XplatUICocoa.cs that needs to be fixed. Only wondering if it should be PostMessage instead of SendMessage so that the button up message handling is completed before WM_MOUSELEAVE. I figured out this is what System.Windows.Forms.CarbonInternal.MouseHandler does in TranslateMessage.

filipnavara commented 5 years ago

I will have to go back through the history to see why we did it this way. The problem with PostMessage is that it could potentially result in wrong order in relation to WM_MOUSEMOVE events (eg. receiving WM_MOUSEMOVE before WM_MOUSE_ENTER).

I'll hopefully have more information soon, just wanted to let you know that I am looking into it.

PaulBol commented 5 years ago

Thanks for your feedback. I see your point. I was thinking that PostMessage may be closer to the Windows and Carbon implementation but it may not be worth risking to break something else. I have a workaround in my code so it's fine for me.

filipnavara commented 5 years ago

I'd love it to be closer to the Windows implementation, but emulating it accurately would likely incur too big of a performance penalty. There are two things to consider:

1) How message queues operate on Windows and how are the messages prioritized. Is there an accurate way to translate that into Cocoa? 2) How the TrackMouseEvent API behaves and how is it internally implemented in respect to message prioritization.

Luckily the answers could be found in MSDN, Wine and ReactOS code:

The important part about message queues is documented in the PeekMessage API documentation. If no filter is specified, messages are processed in the following order:

There are separate queues/flags for each of the above categories. According to ReactOS test suite (and probably the Wine one as well; both excelent sources of the precise message sequences used on Windows) and source code the WM_MOUSELEAVE message is indeed posted and as such would have higher priority than any other input messages (such as WM_MOUSEMOVE). Once there are no posted messages (hence no WM_MOUSELEAVE) the internal input messages are delivered to the application. Only at this point the actual window for any WM_MOUSEMOVE message is determined. There's no WM_MOUSE_ENTER on Windows (a concept only present in the Mono WinForms implementation) and the relevant OnMouseEnter messages are generated from the WM_MOUSEMOVE messages. Basically the worst inconsistency that can happen due to the implementation details is that you would receive delayed OnMouseLeave followed by OnMouseEnter after the next mouse move (unless my mental picture is wrong, which could easily be the case).

I didn't investigate how Windows/Wine/ReactOS handle the case of SetCapture/ReleaseCapture APIs and whether the WM_MOUSELEAVE event is generated in that case and how. Most likely ReleaseCapture simply simulates low-level mouse move by 0 pixels and let's the system figure out which messages need to be generated and into which queue.

On Cocoa emulating all this is tricky. We don't have cross-process SendMessage. PostMessage is emulated by creating native Cocoa events and running them through the Cocoa message pump. The Cocoa messages in the pump are processed by the native Cocoa mechanism (ie. routing through NSWindow, NSView and NSResponder chains). We receive the transleted mouse events in MonoView, MonoWindow and WindowsEventResponder - our classes delivered from NSView, NSWindow and NSResponder respectively. The mouse messages (such as WM_MOUSEMOVE) are generated there and delivered using SendMessage to skip going through the message queue again.

Moreover, Cocoa has no concept of "mouse enter" / "mouse leave" events, so they have to emulated. In addition to that the "mouse move" in Cocoa behaves differently from Windows and it's expensive. While Windows sends WM_MOUSEMOVE to one specific control (since Windows don't make distinction between windows and controls) on Cocoa it's delivered to NSWindow and then any further dispatching is handled by the NSWindow. Specifically for mouse move events it uses a concept of tracking areas. We register a tracking area for each MonoView, which unfortunately results in the side effect that the mouse move events are delivered to the all the controls in the hierarchy under the mouse cursor (eg. when a Button is inside a Panel then both underlying MonoView objects would be notified when mouse moves over the button). Converting this to the Windows messages is quite tricky, rather easy to break and technically challenging to get right, unfortunately.

PaulBol commented 5 years ago

Thanks for explaining the background. You clearly have a far more profound knowledge of the message loop implementation. My idea of simply replacing SendMessage with PostMessage apparently means much more of a change than I had thought and is not worth the risk. Should we close this issue?

filipnavara commented 5 years ago

Let's keep the issue open so we can track the problem, but I cannot promise it will be fixed any time soon. We are currently facing some issue internally that may be related to it, but it uses our internal controls, message filters and we didn't isolate it yet.