SDL: win32: Implement popup windows

From 98e864f2864eb0618ec89c7b2cbd61e29e49417d Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Thu, 2 Mar 2023 17:32:18 -0500
Subject: [PATCH] win32: Implement popup windows

---
 src/video/windows/SDL_windowsevents.c |  28 +++++-
 src/video/windows/SDL_windowsvideo.c  |   2 +
 src/video/windows/SDL_windowswindow.c | 133 ++++++++++++++++++++++----
 src/video/windows/SDL_windowswindow.h |   3 +
 4 files changed, 144 insertions(+), 22 deletions(-)

diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index 9529baf637f9..a9f85f843adb 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -351,9 +351,10 @@ static SDL_Scancode WindowsScanCodeToSDLScanCode(LPARAM lParam, WPARAM wParam)
 }
 
 #if !defined(__XBOXONE__) && !defined(__XBOXSERIES__)
-static SDL_bool WIN_ShouldIgnoreFocusClick()
+static SDL_bool WIN_ShouldIgnoreFocusClick(SDL_WindowData *data)
 {
-    return !SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, SDL_FALSE);
+    return !SDL_WINDOW_IS_POPUP(data->window) &&
+           !SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, SDL_FALSE);
 }
 
 static void WIN_CheckWParamMouseButton(SDL_bool bwParamMousePressed, Uint32 mouseFlags, SDL_bool bSwapButtons, SDL_WindowData *data, Uint8 button, SDL_MouseID mouseID)
@@ -372,7 +373,7 @@ static void WIN_CheckWParamMouseButton(SDL_bool bwParamMousePressed, Uint32 mous
             data->focus_click_pending &= ~SDL_BUTTON(button);
             WIN_UpdateClipCursor(data->window);
         }
-        if (WIN_ShouldIgnoreFocusClick()) {
+        if (WIN_ShouldIgnoreFocusClick(data)) {
             return;
         }
     }
@@ -512,7 +513,7 @@ static void WIN_UpdateFocus(SDL_Window *window, SDL_bool expect_focus)
             data->focus_click_pending |= SDL_BUTTON_X2MASK;
         }
 
-        SDL_SetKeyboardFocus(window);
+        SDL_SetKeyboardFocus(data->keyboard_focus ? data->keyboard_focus : window);
 
         /* In relative mode we are guaranteed to have mouse focus if we have keyboard focus */
         if (!SDL_GetMouse()->relative_mode) {
@@ -788,6 +789,13 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
         WIN_UpdateFocus(data->window, !!LOWORD(wParam));
     } break;
 
+    case WM_MOUSEACTIVATE:
+    {
+        if (SDL_WINDOW_IS_POPUP(data->window)) {
+            return MA_NOACTIVATE;
+        }
+    } break;
+
     case WM_SETFOCUS:
     {
         /* Update the focus in case it's changing between top-level windows in the same application */
@@ -1234,6 +1242,7 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
 
     case WM_WINDOWPOSCHANGED:
     {
+        SDL_Window *win;
         RECT rect;
         int x, y;
         int w, h;
@@ -1260,6 +1269,7 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
         y = rect.top;
         WIN_ScreenPointToSDL(&x, &y);
 
+        SDL_GlobalToRelativeForWindow(data->window, x, y, &x, &y);
         SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_MOVED, x, y);
 
         // Moving the window from one display to another can change the size of the window (in the handling of SDL_EVENT_WINDOW_MOVED), so we need to re-query the bounds
@@ -1297,6 +1307,11 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
             /* Display changed, check ICC profile */
             WIN_UpdateWindowICCProfile(data->window, SDL_TRUE);
         }
+
+        /* Update the position of any child windows */
+        for (win = data->window->first_child; win != NULL; win = win->next_sibling) {
+            WIN_SetWindowPositionInternal(win, SWP_NOCOPYBITS | SWP_NOACTIVATE);
+        }
     } break;
 
     case WM_SIZE:
@@ -1507,6 +1522,11 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
     case WM_NCHITTEST:
     {
         SDL_Window *window = data->window;
+
+        if (window->flags & SDL_WINDOW_TOOLTIP) {
+            return HTTRANSPARENT;
+        }
+
         if (window->hit_test) {
             POINT winpoint;
             winpoint.x = GET_X_LPARAM(lParam);
diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c
index 519c1ef204c6..0a9200c951f2 100644
--- a/src/video/windows/SDL_windowsvideo.c
+++ b/src/video/windows/SDL_windowsvideo.c
@@ -260,6 +260,8 @@ static SDL_VideoDevice *WIN_CreateDevice(void)
 
     device->free = WIN_DeleteDevice;
 
+    device->quirk_flags = VIDEO_DEVICE_QUIRK_HAS_POPUP_WINDOW_SUPPORT;
+
     return device;
 }
 
diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c
index c9ab6d480ce6..e9f758dde3f8 100644
--- a/src/video/windows/SDL_windowswindow.c
+++ b/src/video/windows/SDL_windowswindow.c
@@ -79,7 +79,9 @@ static DWORD GetWindowStyle(SDL_Window *window)
 {
     DWORD style = 0;
 
-    if (window->flags & SDL_WINDOW_FULLSCREEN) {
+    if (SDL_WINDOW_IS_POPUP(window)) {
+        style |= WS_POPUP;
+    } else if (window->flags & SDL_WINDOW_FULLSCREEN) {
         style |= STYLE_FULLSCREEN;
     } else {
         if (window->flags & SDL_WINDOW_BORDERLESS) {
@@ -114,6 +116,16 @@ static DWORD GetWindowStyle(SDL_Window *window)
     return style;
 }
 
+static DWORD GetWindowStyleEx(SDL_Window *window)
+{
+    DWORD style = WS_EX_APPWINDOW;
+
+    if (SDL_WINDOW_IS_POPUP(window)) {
+        style = WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE;
+    }
+    return style;
+}
+
 /**
  * Returns arguments to pass to SetWindowPos - the window rect, including frame, in Windows coordinates.
  * Can be called before we have a HWND.
@@ -128,8 +140,10 @@ static void WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL
 #endif
 
     /* Client rect, in SDL screen coordinates */
-    *x = (use_current ? window->x : window->windowed.x);
-    *y = (use_current ? window->y : window->windowed.y);
+    SDL_RelativeToGlobalForWindow(window,
+                                  (use_current ? window->x : window->windowed.x),
+                                  (use_current ? window->y : window->windowed.y),
+                                  x, y);
     *width = (use_current ? window->w : window->windowed.w);
     *height = (use_current ? window->h : window->windowed.h);
 
@@ -222,8 +236,9 @@ static void WIN_AdjustWindowRect(SDL_Window *window, int *x, int *y, int *width,
     WIN_AdjustWindowRectWithStyle(window, style, menu, x, y, width, height, use_current);
 }
 
-static void WIN_SetWindowPositionInternal(_THIS, SDL_Window *window, UINT flags)
+void WIN_SetWindowPositionInternal(SDL_Window *window, UINT flags)
 {
+    SDL_Window *child_window;
     SDL_WindowData *data = window->driverdata;
     HWND hwnd = data->hwnd;
     HWND top;
@@ -242,6 +257,11 @@ static void WIN_SetWindowPositionInternal(_THIS, SDL_Window *window, UINT flags)
     data->expected_resize = SDL_TRUE;
     SetWindowPos(hwnd, top, x, y, w, h, flags);
     data->expected_resize = SDL_FALSE;
+
+    /* Update any child windows */
+    for (child_window = window->first_child; child_window != NULL; child_window = child_window->next_sibling) {
+        WIN_SetWindowPositionInternal(child_window, flags);
+    }
 }
 
 static void SDLCALL WIN_MouseRelativeModeCenterChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
@@ -304,7 +324,7 @@ static int SetupWindowData(_THIS, SDL_Window *window, HWND hwnd, HWND parent, SD
     data->hwnd = hwnd;
     data->parent = parent;
 #if defined(__XBOXONE__) || defined(__XBOXSERIES__)
-    data->hdc = (HDC) data->hwnd;
+    data->hdc = (HDC)data->hwnd;
 #else
     data->hdc = GetDC(hwnd);
 #endif
@@ -445,6 +465,10 @@ static int SetupWindowData(_THIS, SDL_Window *window, HWND hwnd, HWND parent, SD
         window->flags |= SDL_WINDOW_ALLOW_HIGHDPI;
     }
 
+    if (data->parent && !window->parent) {
+        data->destroy_parent_with_window = SDL_TRUE;
+    }
+
     data->initializing = SDL_FALSE;
 
     /* All done! */
@@ -470,7 +494,7 @@ static void CleanupWindowData(_THIS, SDL_Window *window)
 #endif
         if (data->created) {
             DestroyWindow(data->hwnd);
-            if (data->parent) {
+            if (data->destroy_parent_with_window && data->parent) {
                 DestroyWindow(data->parent);
             }
         } else {
@@ -490,29 +514,81 @@ static void CleanupWindowData(_THIS, SDL_Window *window)
     window->driverdata = NULL;
 }
 
+static void WIN_ConstrainPopup(SDL_Window *window)
+{
+    /* Clamp popup windows to the output borders */
+    if (SDL_WINDOW_IS_POPUP(window)) {
+        SDL_Window *w;
+        SDL_DisplayID displayID;
+        SDL_Rect rect;
+        int abs_x = window->x;
+        int abs_y = window->y;
+        int offset_x = 0, offset_y = 0;
+
+        /* Calculate the total offset from the parents */
+        for (w = window->parent; w->parent != NULL; w = w->parent) {
+            offset_x += w->x;
+            offset_y += w->y;
+        }
+
+        offset_x += w->x;
+        offset_y += w->y;
+        abs_x += offset_x;
+        abs_y += offset_y;
+
+        /* Constrain the popup window to the display of the toplevel parent */
+        displayID = SDL_GetDisplayForWindow(w);
+        SDL_GetDisplayBounds(displayID, &rect);
+        if (abs_x + window->w > rect.x + rect.w) {
+            abs_x -= (abs_x + window->w) - (rect.x + rect.w);
+        }
+        if (abs_y + window->h > rect.y + rect.h) {
+            abs_y -= (abs_y + window->h) - (rect.y + rect.h);
+        }
+        abs_x = SDL_max(abs_x, rect.x);
+        abs_y = SDL_max(abs_y, rect.y);
+
+        window->x = window->windowed.x = abs_x - offset_x;
+        window->y = window->windowed.y = abs_y - offset_y;
+    }
+}
+
+static void WIN_SetKeyboardFocus(SDL_Window *window)
+{
+    SDL_Window *topmost = window;
+
+    /* Find the topmost parent */
+    while (topmost->parent != NULL) {
+    topmost = topmost->parent;
+    }
+
+    topmost->driverdata->keyboard_focus = window;
+    SDL_SetKeyboardFocus(window);
+}
+
 int WIN_CreateWindow(_THIS, SDL_Window *window)
 {
     HWND hwnd, parent = NULL;
     DWORD style = STYLE_BASIC;
+    DWORD styleEx = 0;
     int x, y;
     int w, h;
 
-    if (window->flags & SDL_WINDOW_SKIP_TASKBAR) {
+    if (SDL_WINDOW_IS_POPUP(window)) {
+        parent = window->parent->driverdata->hwnd;
+    } else if (window->flags & SDL_WINDOW_SKIP_TASKBAR) {
         parent = CreateWindow(SDL_Appname, TEXT(""), STYLE_BASIC, 0, 0, 32, 32, NULL, NULL, SDL_Instance, NULL);
     }
 
     style |= GetWindowStyle(window);
+    styleEx |= GetWindowStyleEx(window);
 
     /* Figure out what the window area will be */
+    WIN_ConstrainPopup(window);
     WIN_AdjustWindowRectWithStyle(window, style, FALSE, &x, &y, &w, &h, SDL_FALSE);
 
-    if (window->undefined_x && window->undefined_y &&
-        window->last_displayID == SDL_GetPrimaryDisplay()) {
-        x = CW_USEDEFAULT;
-        y = CW_USEDEFAULT; /* Not actually used */
-    }
-
-    hwnd = CreateWindow(SDL_Appname, TEXT(""), style, x, y, w, h, parent, NULL, SDL_Instance, NULL);
+    hwnd = CreateWindowEx(styleEx, SDL_Appname, TEXT(""), style,
+                          x, y, w, h, parent, NULL, SDL_Instance, NULL);
     if (!hwnd) {
         return WIN_SetError("Couldn't create window");
     }
@@ -711,12 +787,13 @@ void WIN_SetWindowPosition(_THIS, SDL_Window *window)
     /* HighDPI support: removed SWP_NOSIZE. If the move results in a DPI change, we need to allow
      * the window to resize (e.g. AdjustWindowRectExForDpi frame sizes are different).
      */
-    WIN_SetWindowPositionInternal(_this, window, SWP_NOCOPYBITS | SWP_NOACTIVATE);
+    WIN_ConstrainPopup(window);
+    WIN_SetWindowPositionInternal(window, SWP_NOCOPYBITS | SWP_NOACTIVATE);
 }
 
 void WIN_SetWindowSize(_THIS, SDL_Window *window)
 {
-    WIN_SetWindowPositionInternal(_this, window, SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOACTIVATE);
+    WIN_SetWindowPositionInternal(window, SWP_NOCOPYBITS | SWP_NOMOVE | SWP_NOACTIVATE);
 }
 
 int WIN_GetWindowBordersSize(_THIS, SDL_Window *window, int *top, int *left, int *bottom, int *right)
@@ -814,12 +891,32 @@ void WIN_ShowWindow(_THIS, SDL_Window *window)
         nCmdShow = SW_SHOWNOACTIVATE;
     }
     ShowWindow(hwnd, nCmdShow);
+
+    if (window->flags & SDL_WINDOW_POPUP_MENU) {
+        if (window->parent == SDL_GetKeyboardFocus()) {
+            WIN_SetKeyboardFocus(window);
+        }
+    }
 }
 
 void WIN_HideWindow(_THIS, SDL_Window *window)
 {
     HWND hwnd = window->driverdata->hwnd;
     ShowWindow(hwnd, SW_HIDE);
+
+    /* Transfer keyboard focus back to the parent */
+    if (window->flags & SDL_WINDOW_POPUP_MENU) {
+        if (window == SDL_GetKeyboardFocus()) {
+            SDL_Window *new_focus = window->parent;
+
+            /* Find the highest level window that isn't being hidden or destroyed. */
+            while (new_focus->parent != NULL && (new_focus->is_hiding || new_focus->is_destroying)) {
+                new_focus = new_focus->parent;
+            }
+
+            WIN_SetKeyboardFocus(new_focus);
+        }
+    }
 }
 
 void WIN_RaiseWindow(_THIS, SDL_Window *window)
@@ -885,7 +982,7 @@ void WIN_SetWindowBordered(_THIS, SDL_Window *window, SDL_bool bordered)
 
     data->in_border_change = SDL_TRUE;
     SetWindowLong(hwnd, GWL_STYLE, style);
-    WIN_SetWindowPositionInternal(_this, window, SWP_NOCOPYBITS | SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE);
+    WIN_SetWindowPositionInternal(window, SWP_NOCOPYBITS | SWP_FRAMECHANGED | SWP_NOZORDER | SWP_NOACTIVATE);
     data->in_border_change = SDL_FALSE;
 }
 
@@ -1228,7 +1325,7 @@ void WIN_OnWindowEnter(_THIS, SDL_Window *window)
     }
 
     if (window->flags & SDL_WINDOW_ALWAYS_ON_TOP) {
-        WIN_SetWindowPositionInternal(_this, window, SWP_NOCOPYBITS | SWP_NOSIZE | SWP_NOACTIVATE);
+        WIN_SetWindowPositionInternal(window, SWP_NOCOPYBITS | SWP_NOSIZE | SWP_NOACTIVATE);
     }
 }
 
diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h
index 9e31e583c629..2e345b012062 100644
--- a/src/video/windows/SDL_windowswindow.h
+++ b/src/video/windows/SDL_windowswindow.h
@@ -62,8 +62,10 @@ struct SDL_WindowData
     RECT cursor_clipped_rect;
     SDL_Point last_raw_mouse_position;
     SDL_bool mouse_tracked;
+    SDL_bool destroy_parent_with_window;
     SDL_DisplayID last_displayID;
     WCHAR *ICMFileName;
+    SDL_Window *keyboard_focus;
     struct SDL_VideoData *videodata;
 #if SDL_VIDEO_OPENGL_EGL
     EGLSurface egl_surface;
@@ -111,6 +113,7 @@ extern void WIN_ClientPointFromSDLFloat(const SDL_Window *window, float x, float
 extern void WIN_AcceptDragAndDrop(SDL_Window *window, SDL_bool accept);
 extern int WIN_FlashWindow(_THIS, SDL_Window *window, SDL_FlashOperation operation);
 extern void WIN_UpdateDarkModeForHWND(HWND hwnd);
+extern void WIN_SetWindowPositionInternal(SDL_Window *window, UINT flags);
 
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus