SDL: video: Add the concept of child popup windows

From 56ed06e7aa7946386c1856654d1c6c3c9c25d98c Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sun, 26 Feb 2023 14:16:05 -0500
Subject: [PATCH] video: Add the concept of child popup windows

Add the CreatePopupWindow function to allow the creation of child tooltip and menu popup windows. Popup windows must be created as either a tooltip or popup menu and cannot be minimized, maximized, made fullscreen, or grab the mouse.

Child popup windows are tracked and will be recursively hidden, shown, or destroyed in tandem with the parent window.
---
 include/SDL3/SDL_video.h          |  47 ++++++++++++++
 src/dynapi/SDL_dynapi.sym         |   1 +
 src/dynapi/SDL_dynapi_overrides.h |   1 +
 src/dynapi/SDL_dynapi_procs.h     |   1 +
 src/video/SDL_sysvideo.h          |  10 +++
 src/video/SDL_video.c             | 103 +++++++++++++++++++++++++++++-
 6 files changed, 161 insertions(+), 2 deletions(-)

diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 749f2e999643..4e62e98efb59 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -637,11 +637,58 @@ extern DECLSPEC Uint32 SDLCALL SDL_GetWindowPixelFormat(SDL_Window *window);
  *
  * \since This function is available since SDL 3.0.0.
  *
+ * \sa SDL_CreatePopupWindow
  * \sa SDL_CreateWindowFrom
  * \sa SDL_DestroyWindow
  */
 extern DECLSPEC SDL_Window *SDLCALL SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags);
 
+/**
+ * Create a child popup window of the specified parent window.
+ *
+ * 'flags' **must** contain exactly one of the following:
+ * - 'SDL_WINDOW_TOOLTIP': The popup window is a tooltip and will not pass any input events
+ * - 'SDL_WINDOW_POPUP_MENU': The popup window is a popup menu
+ *
+ * The following flags are not valid for popup windows and will be ignored:
+ * - 'SDL_WINDOW_MINIMIZED'
+ * - 'SDL_WINDOW_MAXIMIZED'
+ * - 'SDL_WINDOW_FULLSCREEN'
+ * - `SDL_WINDOW_BORDERLESS`
+ * - `SDL_WINDOW_MOUSE_GRABBED`
+ *
+ * The parent parameter **must** be non-null and a valid window.
+ * The parent of a popup window can be either a regular, toplevel window,
+ * or another popup window.
+ *
+ * Popup windows cannot be minimized, maximized, made fullscreen, or grab
+ * the mouse. Attempts to do so will fail.
+ *
+ * If a parent window is hidden, any child popup windows will be recursively
+ * hidden as well. Child popup windows not explicitly hidden will be restored
+ * when the parent is shown.
+ *
+ * If the parent window is destroyed, any child popup windows will be
+ * recursively destroyed as well.
+ *
+ * \param parent the parent of the window, must not be NULL
+ * \param x the x position of the popup window relative to the parent,
+ *          in screen coordinates
+ * \param y the y position of the popup window relative to the parent,
+ *          in screen coordinates
+ * \param w the width of the window, in screen coordinates
+ * \param h the height of the window, in screen coordinates
+ * \param flags 0, or one or more SDL_WindowFlags OR'd together
+ * \returns the window that was created or NULL on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_CreateWindow
+ * \sa SDL_DestroyWindow
+ */
+extern DECLSPEC SDL_Window *SDLCALL SDL_CreatePopupWindow(SDL_Window *parent, int x, int y, int w, int h, Uint32 flags);
+
 /**
  * Create an SDL window from an existing native window.
  *
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index a9f6680f36bb..e7c4f0bc151e 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -838,6 +838,7 @@ SDL3_0.0.0 {
     SDL_SetRenderScale;
     SDL_GetRenderScale;
     SDL_GetRenderWindowSize;
+    SDL_CreatePopupWindow;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index f45dbbfebe3f..664eb34c6c1d 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -865,3 +865,4 @@
 #define SDL_SetRenderScale SDL_SetRenderScale_REAL
 #define SDL_GetRenderScale SDL_GetRenderScale_REAL
 #define SDL_GetRenderWindowSize SDL_GetRenderWindowSize_REAL
+#define SDL_CreatePopupWindow SDL_CreatePopupWindow_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index cb0e4caf41ae..d50576b683fc 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -910,3 +910,4 @@ SDL_DYNAPI_PROC(int,SDL_ConvertEventToRenderCoordinates,(SDL_Renderer *a, SDL_Ev
 SDL_DYNAPI_PROC(int,SDL_SetRenderScale,(SDL_Renderer *a, float b, float c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_GetRenderScale,(SDL_Renderer *a, float *b, float *c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_GetRenderWindowSize,(SDL_Renderer *a, int *b, int *c),(a,b,c),return)
+SDL_DYNAPI_PROC(SDL_Window*,SDL_CreatePopupWindow,(SDL_Window *a, int b, int c, int d, int e, Uint32 f),(a,b,c,d,e,f),return)
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 1a6e4c5ba0cc..9d298d38a68f 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -98,6 +98,7 @@ struct SDL_Window
     SDL_bool surface_valid;
 
     SDL_bool is_hiding;
+    SDL_bool restore_on_show; /* Child was hidden recursively by the parent, restore when shown. */
     SDL_bool is_destroying;
     SDL_bool is_dropping; /* drag/drop in progress, expecting SDL_SendDropComplete(). */
 
@@ -114,12 +115,21 @@ struct SDL_Window
 
     SDL_Window *prev;
     SDL_Window *next;
+
+    SDL_Window *parent;
+    SDL_Window *first_child;
+    SDL_Window *prev_sibling;
+    SDL_Window *next_sibling;
 };
 #define SDL_WINDOW_FULLSCREEN_VISIBLE(W)        \
     ((((W)->flags & SDL_WINDOW_FULLSCREEN) != 0) &&   \
      (((W)->flags & SDL_WINDOW_HIDDEN) == 0) && \
      (((W)->flags & SDL_WINDOW_MINIMIZED) == 0))
 
+#define SDL_WINDOW_IS_POPUP(W)                   \
+    ((((W)->flags & SDL_WINDOW_TOOLTIP) != 0) || \
+    (((W)->flags & SDL_WINDOW_POPUP_MENU) != 0)) \
+                                                 \
 /*
  * Define the SDL display structure.
  * This corresponds to physical monitors attached to the system.
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 431adfde83e7..89f63fd96e50 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -145,6 +145,12 @@ static VideoBootStrap *bootstrap[] = {
         return retval;                                                  \
     }                                                                   \
 
+#define CHECK_WINDOW_NOT_POPUP(window, retval)                          \
+    if (SDL_WINDOW_IS_POPUP(window)) {                                  \
+        SDL_SetError("Operation invalid on popup windows");             \
+        return retval;                                                  \
+    }
+
 #if defined(__MACOS__) && defined(SDL_VIDEO_DRIVER_COCOA)
 /* Support for macOS fullscreen spaces */
 extern SDL_bool Cocoa_IsWindowInFullscreenSpace(SDL_Window *window);
@@ -1481,6 +1487,7 @@ static int SDL_UpdateFullscreenMode(SDL_Window *window, SDL_bool fullscreen)
 int SDL_SetWindowFullscreenMode(SDL_Window *window, const SDL_DisplayMode *mode)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (mode) {
         if (!SDL_GetFullscreenModeMatch(mode)) {
@@ -1506,6 +1513,7 @@ int SDL_SetWindowFullscreenMode(SDL_Window *window, const SDL_DisplayMode *mode)
 const SDL_DisplayMode *SDL_GetWindowFullscreenMode(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, NULL);
+    CHECK_WINDOW_NOT_POPUP(window, NULL);
 
     if (window->flags & SDL_WINDOW_FULLSCREEN) {
         return SDL_GetFullscreenModeMatch(&window->current_fullscreen_mode);
@@ -1613,7 +1621,7 @@ static int SDL_DllNotSupported(const char *name)
     return SDL_SetError("No dynamic %s support in current SDL video driver (%s)", name, _this->name);
 }
 
-SDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags)
+static SDL_Window *SDL_CreateWindowInternal(const char *title, int x, int y, int w, int h, SDL_Window *parent, Uint32 flags)
 {
     SDL_Window *window;
     Uint32 type_flags, graphics_flags;
@@ -1642,6 +1650,12 @@ SDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint
         return NULL;
     }
 
+    /* Tooltip and popup menu window must specify a parent window */
+    if (!parent && ((type_flags & SDL_WINDOW_TOOLTIP) || (type_flags & SDL_WINDOW_POPUP_MENU))) {
+        SDL_SetError("Tooltip and popup menu windows must specify a parent window");
+        return NULL;
+    }
+
     /* Some platforms can't create zero-sized windows */
     if (w < 1) {
         w = 1;
@@ -1757,6 +1771,16 @@ SDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint
     }
     _this->windows = window;
 
+    if (parent) {
+        window->parent = parent;
+
+        window->next_sibling = parent->first_child;
+        if (parent->first_child) {
+            parent->first_child->prev = window;
+        }
+        parent->first_child = window;
+    }
+
     if (_this->CreateSDLWindow && _this->CreateSDLWindow(_this, window) < 0) {
         SDL_DestroyWindow(window);
         return NULL;
@@ -1794,6 +1818,28 @@ SDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint
     return window;
 }
 
+SDL_Window *SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags)
+{
+    return SDL_CreateWindowInternal(title, x, y, w , h, NULL, flags);
+}
+
+SDL_Window *SDL_CreatePopupWindow(SDL_Window *parent, int x, int y, int w, int h, Uint32 flags)
+{
+    /* Parent must be a valid window */
+    CHECK_WINDOW_MAGIC(parent, NULL);
+
+    /* Remove invalid flags */
+    flags &= ~(SDL_WINDOW_MINIMIZED | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MOUSE_GRABBED);
+
+    /* Popups must specify either the tooltip or popup menu window flags */
+    if ((flags & SDL_WINDOW_TOOLTIP) || (flags & SDL_WINDOW_POPUP_MENU)) {
+        return SDL_CreateWindowInternal(NULL, x, y, w, h, parent, flags);
+    }
+
+    SDL_SetError("Popup windows must specify either the 'SDL_WINDOW_TOOLTIP' or the 'SDL_WINDOW_POPUP_MENU' flag");
+    return NULL;
+}
+
 SDL_Window *SDL_CreateWindowFrom(const void *data)
 {
     SDL_Window *window;
@@ -2037,6 +2083,7 @@ Uint32 SDL_GetWindowFlags(SDL_Window *window)
 int SDL_SetWindowTitle(SDL_Window *window, const char *title)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (title == window->title) {
         return 0;
@@ -2255,6 +2302,7 @@ int SDL_GetWindowPosition(SDL_Window *window, int *x, int *y)
 int SDL_SetWindowBordered(SDL_Window *window, SDL_bool bordered)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
     if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
         const int want = (bordered != SDL_FALSE); /* normalize the flag. */
         const int have = !(window->flags & SDL_WINDOW_BORDERLESS);
@@ -2273,6 +2321,7 @@ int SDL_SetWindowBordered(SDL_Window *window, SDL_bool bordered)
 int SDL_SetWindowResizable(SDL_Window *window, SDL_bool resizable)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
     if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
         const int want = (resizable != SDL_FALSE); /* normalize the flag. */
         const int have = ((window->flags & SDL_WINDOW_RESIZABLE) != 0);
@@ -2291,6 +2340,7 @@ int SDL_SetWindowResizable(SDL_Window *window, SDL_bool resizable)
 int SDL_SetWindowAlwaysOnTop(SDL_Window *window, SDL_bool on_top)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
     if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
         const int want = (on_top != SDL_FALSE); /* normalize the flag. */
         const int have = ((window->flags & SDL_WINDOW_ALWAYS_ON_TOP) != 0);
@@ -2509,27 +2559,54 @@ int SDL_GetWindowMaximumSize(SDL_Window *window, int *max_w, int *max_h)
 
 int SDL_ShowWindow(SDL_Window *window)
 {
+    SDL_Window *child;
     CHECK_WINDOW_MAGIC(window, -1);
 
     if (!(window->flags & SDL_WINDOW_HIDDEN)) {
         return 0;
     }
 
+    /* If the parent is hidden, set the flag to restore this when the parent is shown */
+    if (window->parent && (window->parent->flags & SDL_WINDOW_HIDDEN)) {
+        window->restore_on_show = SDL_TRUE;
+        return 0;
+    }
+
     if (_this->ShowWindow) {
         _this->ShowWindow(_this, window);
     }
     SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_SHOWN, 0, 0);
+
+    /* Restore child windows */
+    for (child = window->first_child; child != NULL; child = child->next_sibling) {
+        if (!child->restore_on_show && (child->flags & SDL_WINDOW_HIDDEN)) {
+            break;
+        }
+        SDL_ShowWindow(child);
+        child->restore_on_show = SDL_FALSE;
+    }
     return 0;
 }
 
 int SDL_HideWindow(SDL_Window *window)
 {
+    SDL_Window *child;
     CHECK_WINDOW_MAGIC(window, -1);
 
     if (window->flags & SDL_WINDOW_HIDDEN) {
+        window->restore_on_show = SDL_FALSE;
         return 0;
     }
 
+    /* Hide all child windows */
+    for (child = window->first_child; child != NULL; child = child->next_sibling) {
+        if (child->flags & SDL_WINDOW_HIDDEN) {
+            break;
+        }
+        SDL_HideWindow(child);
+        child->restore_on_show = SDL_TRUE;
+    }
+
     window->is_hiding = SDL_TRUE;
     SDL_UpdateFullscreenMode(window, SDL_FALSE);
 
@@ -2557,6 +2634,7 @@ int SDL_RaiseWindow(SDL_Window *window)
 int SDL_MaximizeWindow(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (window->flags & SDL_WINDOW_MAXIMIZED) {
         return 0;
@@ -2572,7 +2650,7 @@ int SDL_MaximizeWindow(SDL_Window *window)
 
 static SDL_bool SDL_CanMinimizeWindow(SDL_Window *window)
 {
-    if (!_this->MinimizeWindow) {
+    if (!_this->MinimizeWindow || SDL_WINDOW_IS_POPUP(window)) {
         return SDL_FALSE;
     }
     return SDL_TRUE;
@@ -2581,6 +2659,7 @@ static SDL_bool SDL_CanMinimizeWindow(SDL_Window *window)
 int SDL_MinimizeWindow(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (window->flags & SDL_WINDOW_MINIMIZED) {
         return 0;
@@ -2603,6 +2682,7 @@ int SDL_MinimizeWindow(SDL_Window *window)
 int SDL_RestoreWindow(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (!(window->flags & (SDL_WINDOW_MAXIMIZED | SDL_WINDOW_MINIMIZED))) {
         return 0;
@@ -2620,6 +2700,7 @@ int SDL_SetWindowFullscreen(SDL_Window *window, SDL_bool fullscreen)
     Uint32 flags = fullscreen ? SDL_WINDOW_FULLSCREEN : 0;
 
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     if (flags == (window->flags & SDL_WINDOW_FULLSCREEN)) {
         return 0;
@@ -2880,6 +2961,7 @@ void SDL_UpdateWindowGrab(SDL_Window *window)
 int SDL_SetWindowGrab(SDL_Window *window, SDL_bool grabbed)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+    CHECK_WINDOW_NOT_POPUP(window, -1);
 
     SDL_SetWindowMouseGrab(window, grabbed);
 
@@ -3166,6 +3248,23 @@ void SDL_DestroyWindow(SDL_Window *window)
 
     window->is_destroying = SDL_TRUE;
 
+    /* Destroy any child windows of this window */
+    while (window->first_child) {
+        SDL_DestroyWindow(window->first_child);
+    }
+
+    /* If this is a child window, unlink it from its siblings */
+    if (window->parent) {
+        if (window->next_sibling) {
+            window->next_sibling->prev_sibling = window->prev_sibling;
+        }
+        if (window->prev_sibling) {
+            window->prev_sibling->next_sibling = window->next_sibling;
+        } else {
+            window->parent->first_child = window->next_sibling;
+        }
+    }
+
     /* Restore video mode, etc. */
     if (!(window->flags & SDL_WINDOW_FOREIGN)) {
         SDL_HideWindow(window);