SDL: tray: fixed multi-threading issues with GTk implementation

From 5f2dd5f04efdeda65a52b77edfc7ee8748b34901 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 19 Jan 2025 16:33:05 -0800
Subject: [PATCH] tray: fixed multi-threading issues with GTk implementation

GTK+ documentation states that all GDK and GTK+ calls should be made from the main thread.

Fixes https://github.com/libsdl-org/SDL/issues/11984
---
 include/SDL3/SDL_tray.h           | 11 ++++
 src/dynapi/SDL_dynapi.sym         |  1 +
 src/dynapi/SDL_dynapi_overrides.h |  1 +
 src/dynapi/SDL_dynapi_procs.h     |  1 +
 src/events/SDL_events.c           |  2 +
 src/tray/cocoa/SDL_tray.m         |  4 ++
 src/tray/dummy/SDL_tray.c         |  4 ++
 src/tray/unix/SDL_tray.c          | 83 ++++++++++++++-----------------
 src/tray/windows/SDL_tray.c       |  4 ++
 9 files changed, 64 insertions(+), 47 deletions(-)

diff --git a/include/SDL3/SDL_tray.h b/include/SDL3/SDL_tray.h
index 54123b5020342..f1fbc01cb5c94 100644
--- a/include/SDL3/SDL_tray.h
+++ b/include/SDL3/SDL_tray.h
@@ -498,6 +498,17 @@ extern SDL_DECLSPEC SDL_TrayEntry *SDLCALL SDL_GetTrayMenuParentEntry(SDL_TrayMe
  */
 extern SDL_DECLSPEC SDL_Tray *SDLCALL SDL_GetTrayMenuParentTray(SDL_TrayMenu *menu);
 
+/**
+ * Update the trays.
+ *
+ * This is called automatically by the event loop and is only needed if you're using trays but aren't handling SDL events.
+ *
+ * \since This function is available since SDL 3.2.0.
+ *
+ * \threadsafety This function should only be called on the main thread.
+ */
+extern SDL_DECLSPEC void SDLCALL SDL_UpdateTrays(void);
+
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
 }
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index f0e66fc8348d8..b4dcc38396bb1 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1232,6 +1232,7 @@ SDL3_0.0.0 {
     SDL_GetThreadState;
     SDL_AudioStreamDevicePaused;
     SDL_ClickTrayEntry;
+    SDL_UpdateTrays;
     # 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 e23fe2ed2841b..d90b6fd074b54 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1257,3 +1257,4 @@
 #define SDL_GetThreadState SDL_GetThreadState_REAL
 #define SDL_AudioStreamDevicePaused SDL_AudioStreamDevicePaused_REAL
 #define SDL_ClickTrayEntry SDL_ClickTrayEntry_REAL
+#define SDL_UpdateTrays SDL_UpdateTrays_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index d74f9597c694a..e57603d185522 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1265,3 +1265,4 @@ SDL_DYNAPI_PROC(SDL_Tray*,SDL_GetTrayMenuParentTray,(SDL_TrayMenu *a),(a),return
 SDL_DYNAPI_PROC(SDL_ThreadState,SDL_GetThreadState,(SDL_Thread *a),(a),return)
 SDL_DYNAPI_PROC(bool,SDL_AudioStreamDevicePaused,(SDL_AudioStream *a),(a),return)
 SDL_DYNAPI_PROC(void,SDL_ClickTrayEntry,(SDL_TrayEntry *a),(a),)
+SDL_DYNAPI_PROC(void,SDL_UpdateTrays,(void),(),)
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index f7b598304b3ab..c82fed18b7a48 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -1399,6 +1399,8 @@ static void SDL_PumpEventsInternal(bool push_sentinel)
     }
 #endif
 
+    SDL_UpdateTrays();
+
     SDL_SendPendingSignalEvents(); // in case we had a signal handler fire, etc.
 
     if (push_sentinel && SDL_EventEnabled(SDL_EVENT_POLL_SENTINEL)) {
diff --git a/src/tray/cocoa/SDL_tray.m b/src/tray/cocoa/SDL_tray.m
index 9d2f55e2db347..ae8d6be835f27 100644
--- a/src/tray/cocoa/SDL_tray.m
+++ b/src/tray/cocoa/SDL_tray.m
@@ -78,6 +78,10 @@ static void DestroySDLMenu(SDL_TrayMenu *menu)
     SDL_free(menu);
 }
 
+void SDL_UpdateTrays(void)
+{
+}
+
 SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
 {
     if (!SDL_IsMainThread()) {
diff --git a/src/tray/dummy/SDL_tray.c b/src/tray/dummy/SDL_tray.c
index 55a1e645586f4..db76db25269bf 100644
--- a/src/tray/dummy/SDL_tray.c
+++ b/src/tray/dummy/SDL_tray.c
@@ -25,6 +25,10 @@
 
 #include "../SDL_tray_utils.h"
 
+void SDL_UpdateTrays(void)
+{
+}
+
 SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
 {
     SDL_Unsupported();
diff --git a/src/tray/unix/SDL_tray.c b/src/tray/unix/SDL_tray.c
index c26da85a58a61..5f017c2f59c20 100644
--- a/src/tray/unix/SDL_tray.c
+++ b/src/tray/unix/SDL_tray.c
@@ -54,9 +54,10 @@ typedef enum
     G_CONNECT_AFTER = 1 << 0,
     G_CONNECT_SWAPPED = 1 << 1
 } GConnectFlags;
-gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags);
-void (*g_object_unref)(gpointer object);
-gchar *(*g_mkdtemp)(gchar *template);
+
+static gulong (*g_signal_connect_data)(gpointer instance, const gchar *detailed_signal, GCallback c_handler, gpointer data, GClosureNotify destroy_data, GConnectFlags connect_flags);
+static void (*g_object_unref)(gpointer object);
+static gchar *(*g_mkdtemp)(gchar *template);
 
 #define g_signal_connect(instance, detailed_signal, c_handler, data) \
     g_signal_connect_data ((instance), (detailed_signal), (c_handler), (data), NULL, (GConnectFlags) 0)
@@ -78,24 +79,23 @@ typedef struct _GtkMenuShell GtkMenuShell;
 typedef struct _GtkWidget GtkWidget;
 typedef struct _GtkCheckMenuItem GtkCheckMenuItem;
 
-gboolean (*gtk_init_check)(int *argc, char ***argv);
-void (*gtk_main)(void);
-void (*gtk_main_quit)(void);
-GtkWidget* (*gtk_menu_new)(void);
-GtkWidget* (*gtk_separator_menu_item_new)(void);
-GtkWidget* (*gtk_menu_item_new_with_label)(const gchar *label);
-void (*gtk_menu_item_set_submenu)(GtkMenuItem *menu_item, GtkWidget *submenu);
-GtkWidget* (*gtk_check_menu_item_new_with_label)(const gchar *label);
-void (*gtk_check_menu_item_set_active)(GtkCheckMenuItem *check_menu_item, gboolean is_active);
-void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive);
-void (*gtk_widget_show)(GtkWidget *widget);
-void (*gtk_menu_shell_append)(GtkMenuShell *menu_shell, GtkWidget *child);
-void (*gtk_menu_shell_insert)(GtkMenuShell *menu_shell, GtkWidget *child, gint position);
-void (*gtk_widget_destroy)(GtkWidget *widget);
-const gchar *(*gtk_menu_item_get_label)(GtkMenuItem *menu_item);
-void (*gtk_menu_item_set_label)(GtkMenuItem *menu_item, const gchar *label);
-gboolean (*gtk_check_menu_item_get_active)(GtkCheckMenuItem *check_menu_item);
-gboolean (*gtk_widget_get_sensitive)(GtkWidget *widget);
+static gboolean (*gtk_init_check)(int *argc, char ***argv);
+static gboolean (*gtk_main_iteration_do)(gboolean blocking);
+static GtkWidget* (*gtk_menu_new)(void);
+static GtkWidget* (*gtk_separator_menu_item_new)(void);
+static GtkWidget* (*gtk_menu_item_new_with_label)(const gchar *label);
+static void (*gtk_menu_item_set_submenu)(GtkMenuItem *menu_item, GtkWidget *submenu);
+static GtkWidget* (*gtk_check_menu_item_new_with_label)(const gchar *label);
+static void (*gtk_check_menu_item_set_active)(GtkCheckMenuItem *check_menu_item, gboolean is_active);
+static void (*gtk_widget_set_sensitive)(GtkWidget *widget, gboolean sensitive);
+static void (*gtk_widget_show)(GtkWidget *widget);
+static void (*gtk_menu_shell_append)(GtkMenuShell *menu_shell, GtkWidget *child);
+static void (*gtk_menu_shell_insert)(GtkMenuShell *menu_shell, GtkWidget *child, gint position);
+static void (*gtk_widget_destroy)(GtkWidget *widget);
+static const gchar *(*gtk_menu_item_get_label)(GtkMenuItem *menu_item);
+static void (*gtk_menu_item_set_label)(GtkMenuItem *menu_item, const gchar *label);
+static gboolean (*gtk_check_menu_item_get_active)(GtkCheckMenuItem *check_menu_item);
+static gboolean (*gtk_widget_get_sensitive)(GtkWidget *widget);
 
 #define GTK_MENU_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_MENU_ITEM, GtkMenuItem))
 #define GTK_WIDGET(widget) (G_TYPE_CHECK_INSTANCE_CAST ((widget), GTK_TYPE_WIDGET, GtkWidget))
@@ -119,23 +119,17 @@ typedef enum {
 } AppIndicatorStatus;
 
 typedef struct _AppIndicator AppIndicator;
-AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category);
-void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status);
-void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name);
-void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu);
+
+static AppIndicator *(*app_indicator_new)(const gchar *id, const gchar *icon_name, AppIndicatorCategory category);
+static void (*app_indicator_set_status)(AppIndicator *self, AppIndicatorStatus status);
+static void (*app_indicator_set_icon)(AppIndicator *self, const gchar *icon_name);
+static void (*app_indicator_set_menu)(AppIndicator *self, GtkMenu *menu);
+
 /* ------------------------------------------------------------------------- */
 /*                      END THIRD-PARTY HEADER CONTENT                       */
 /* ------------------------------------------------------------------------- */
 #endif
 
-static int main_gtk_thread(void *data)
-{
-    gtk_main();
-    return 0;
-}
-
-static bool gtk_thread_active = false;
-
 #ifdef APPINDICATOR_HEADER
 
 static void quit_gtk(void)
@@ -232,8 +226,7 @@ static bool init_gtk(void)
     }
 
     gtk_init_check = dlsym(libgtk, "gtk_init_check");
-    gtk_main = dlsym(libgtk, "gtk_main");
-    gtk_main_quit = dlsym(libgtk, "gtk_main_quit");
+    gtk_main_iteration_do = dlsym(libgtk, "gtk_main_iteration_do");
     gtk_menu_new = dlsym(libgtk, "gtk_menu_new");
     gtk_separator_menu_item_new = dlsym(libgtk, "gtk_separator_menu_item_new");
     gtk_menu_item_new_with_label = dlsym(libgtk, "gtk_menu_item_new_with_label");
@@ -262,8 +255,7 @@ static bool init_gtk(void)
     app_indicator_set_menu = dlsym(libappindicator, "app_indicator_set_menu");
 
     if (!gtk_init_check ||
-        !gtk_main ||
-        !gtk_main_quit ||
+        !gtk_main_iteration_do ||
         !gtk_menu_new ||
         !gtk_separator_menu_item_new ||
         !gtk_menu_item_new_with_label ||
@@ -396,6 +388,13 @@ static void DestroySDLMenu(SDL_TrayMenu *menu)
     SDL_free(menu);
 }
 
+void SDL_UpdateTrays(void)
+{
+    if (SDL_HasActiveTrays()) {
+        gtk_main_iteration_do(FALSE);
+    }
+}
+
 SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
 {
     if (!SDL_IsMainThread()) {
@@ -407,11 +406,6 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
         return NULL;
     }
 
-    if (!gtk_thread_active) {
-        SDL_DetachThread(SDL_CreateThread(main_gtk_thread, "tray gtk", NULL));
-        gtk_thread_active = true;
-    }
-
     SDL_Tray *tray = (SDL_Tray *)SDL_calloc(1, sizeof(*tray));
     if (!tray) {
         return NULL;
@@ -794,9 +788,4 @@ void SDL_DestroyTray(SDL_Tray *tray)
     }
 
     SDL_free(tray);
-
-    if (!SDL_HasActiveTrays()) {
-        gtk_main_quit();
-        gtk_thread_active = false;
-    }
 }
diff --git a/src/tray/windows/SDL_tray.c b/src/tray/windows/SDL_tray.c
index 0afd62768f0e3..a7e27c069914d 100644
--- a/src/tray/windows/SDL_tray.c
+++ b/src/tray/windows/SDL_tray.c
@@ -209,6 +209,10 @@ static HICON load_default_icon()
     return LoadIcon(NULL, IDI_APPLICATION);
 }
 
+void SDL_UpdateTrays(void)
+{
+}
+
 SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip)
 {
     if (!SDL_IsMainThread()) {