Win32 backend: can't hover over nonclient areas when window does not have focus #6045

Closed
opened 2023-01-05 03:06:45 +00:00 by thundercarrot · 5 comments
thundercarrot commented 2023-01-05 03:06:45 +00:00 (Migrated from github.com)

Version: 1.88
Branch: master/viewport/docking

Back-ends: imgui_impl_win32.cpp
Compiler: vs2022
Operating System: Windows 11

imgui buttons that are part of the nonclient area of the window behave correctly only when the window has focus:

When creating a frameless window with owner draw captions you can use imgui buttons for min/max/close buttons, by appropriately processing WM_NCHITTEST on the button regions. This works fine when the window has focus (is foreground). In this case AddMousePosEvent is not called from ImGui_ImplWin32_WndProcHandler's WM_MOUSEMOVE handler since they are considered nonclient areas. Instead, AddMousePosEvent is called from
ImGui_ImplWin32_UpdateMouseData(). However, UpdateMouseData checks first that the window has focus, so that when the window does not have focus AddMousePosEvent is never called and hover tests fail.

ImGui_ImplWin32_WndProcHandler should probably handle WM_NCMOUSEMOVE as well as WM_MOUSEMOVE.

Version: 1.88 Branch: master/viewport/docking Back-ends: imgui_impl_win32.cpp Compiler: vs2022 Operating System: Windows 11 imgui buttons that are part of the nonclient area of the window behave correctly only when the window has focus: When creating a frameless window with owner draw captions you can use imgui buttons for min/max/close buttons, by appropriately processing WM_NCHITTEST on the button regions. This works fine when the window has focus (is foreground). In this case AddMousePosEvent is not called from ImGui_ImplWin32_WndProcHandler's WM_MOUSEMOVE handler since they are considered nonclient areas. Instead, AddMousePosEvent is called from ImGui_ImplWin32_UpdateMouseData(). However, UpdateMouseData checks first that the window has focus, so that when the window does not have focus AddMousePosEvent is never called and hover tests fail. ImGui_ImplWin32_WndProcHandler should probably handle WM_NCMOUSEMOVE as well as WM_MOUSEMOVE.
ocornut commented 2023-01-05 12:32:14 +00:00 (Migrated from github.com)

ImGui_ImplWin32_WndProcHandler should probably handle WM_NCMOUSEMOVE as well as WM_MOUSEMOVE.

It doesn't seem trivial to handle seems how WM_MOUSELEAVE are occasionally triggered after WM_NCMOUSEMOVE.

I came up with this:

Add bool MouseInClientArea; in ImGui_ImplWin32_Data.

    case WM_MOUSEMOVE:
    case WM_NCMOUSEMOVE:
    {
        // We need to call TrackMouseEvent in order to receive WM_MOUSELEAVE events
        bd->MouseHwnd = hwnd;
        bd->MouseInClientArea = (msg == WM_MOUSEMOVE);
        if (!bd->MouseTracked)
        {
            TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hwnd, 0 };
            ::TrackMouseEvent(&tme);
            bd->MouseTracked = true;
        }
        POINT pos = { (LONG)GET_X_LPARAM(lParam), (LONG)GET_Y_LPARAM(lParam) };
        if (msg == WM_NCMOUSEMOVE && ::ScreenToClient(hwnd, &pos) == FALSE) // WM_NCMOUSEMOVE are provided in screen coordinates.
            break;
        io.AddMousePosEvent((float)pos.x, (float)pos.y);
        break;
    }
    case WM_MOUSELEAVE:
    case WM_NCMOUSELEAVE:
        if (bd->MouseInClientArea == (msg == WM_MOUSELEAVE))
        {
            if (bd->MouseHwnd == hwnd)
                bd->MouseHwnd = nullptr;
            bd->MouseTracked = false;
            io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
        }
        break;

I would however need to test it more thoroughly, and with multi-viewports.
Could you test on your side?
That stuff tends to be tricky and fragile so if it comes with other undesirable side-effects we might need to back-track.

> ImGui_ImplWin32_WndProcHandler should probably handle WM_NCMOUSEMOVE as well as WM_MOUSEMOVE. It doesn't seem trivial to handle seems how WM_MOUSELEAVE are occasionally triggered after WM_NCMOUSEMOVE. I came up with this: Add `bool MouseInClientArea;` in `ImGui_ImplWin32_Data`. ```cpp case WM_MOUSEMOVE: case WM_NCMOUSEMOVE: { // We need to call TrackMouseEvent in order to receive WM_MOUSELEAVE events bd->MouseHwnd = hwnd; bd->MouseInClientArea = (msg == WM_MOUSEMOVE); if (!bd->MouseTracked) { TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE, hwnd, 0 }; ::TrackMouseEvent(&tme); bd->MouseTracked = true; } POINT pos = { (LONG)GET_X_LPARAM(lParam), (LONG)GET_Y_LPARAM(lParam) }; if (msg == WM_NCMOUSEMOVE && ::ScreenToClient(hwnd, &pos) == FALSE) // WM_NCMOUSEMOVE are provided in screen coordinates. break; io.AddMousePosEvent((float)pos.x, (float)pos.y); break; } case WM_MOUSELEAVE: case WM_NCMOUSELEAVE: if (bd->MouseInClientArea == (msg == WM_MOUSELEAVE)) { if (bd->MouseHwnd == hwnd) bd->MouseHwnd = nullptr; bd->MouseTracked = false; io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); } break; ``` I would however need to test it more thoroughly, and with multi-viewports. Could you test on your side? That stuff tends to be tricky and fragile so if it comes with other undesirable side-effects we might need to back-track.
thundercarrot commented 2023-01-05 19:23:52 +00:00 (Migrated from github.com)

In handling WM_NCMOUSEMOVE I think you want TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE | TME_NONCLIENT, hwnd, 0 };

Getting WM_MOUSELEAVE after the first WM_NCMOUSEMOVE is unfortunate but not the end of the world. Otherwise I believe this works. One idea is to cancel the outstanding track mouse event (TME_CANCEL | TME_LEAVE) and issue a new one.

In handling WM_NCMOUSEMOVE I think you want `TRACKMOUSEEVENT tme = { sizeof(tme), TME_LEAVE | TME_NONCLIENT, hwnd, 0 };` Getting WM_MOUSELEAVE after the first WM_NCMOUSEMOVE is unfortunate but not the end of the world. Otherwise I believe this works. One idea is to cancel the outstanding track mouse event (`TME_CANCEL | TME_LEAVE`) and issue a new one.
thundercarrot commented 2023-01-06 06:02:35 +00:00 (Migrated from github.com)

@ocornut your code above works perfectly with the addition of

if (msg == WM_NCMOUSEMOVE) tme.dwFlags |= TME_NONCLIENT;
@ocornut your code above works perfectly with the addition of ``` if (msg == WM_NCMOUSEMOVE) tme.dwFlags |= TME_NONCLIENT; ```
ocornut commented 2023-02-15 17:20:02 +00:00 (Migrated from github.com)

your code above works perfectly with the addition of

That didn't look right. When unfocused we don't always get leave events there.

I think I worked it our right always sending a cancel request when changing area.
Pushed 5d74891
Merged in docking via e25e452

cc @BobbyAnguelov would be good if you can confirm it works with vanilla backend and without your custom msg handler.

> your code above works perfectly with the addition of That didn't look right. When unfocused we don't always get leave events there. I think I worked it our right always sending a cancel request when changing area. Pushed 5d74891 Merged in docking via e25e452 cc @BobbyAnguelov would be good if you can confirm it works with vanilla backend and without your custom msg handler.
BobbyAnguelov commented 2023-02-18 19:20:55 +00:00 (Migrated from github.com)

Works in both the minimal example I created and in Esoterica.

Works in both the minimal example I created and in Esoterica.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ocornut/imgui#6045
No description provided.