SDL: Add progress bar support for Linux

From 3f2226a917a2a3aefb8daabd42d939099f16f28d Mon Sep 17 00:00:00 2001
From: GamesTrap <[EMAIL REDACTED]>
Date: Wed, 7 May 2025 00:16:16 +0200
Subject: [PATCH] Add progress bar support for Linux

---
 CMakeLists.txt                       |   1 +
 docs/README-wayland.md               |   9 ++
 src/core/linux/SDL_dbus.c            |   1 +
 src/core/linux/SDL_dbus.h            |   1 +
 src/core/linux/SDL_progressbar.c     | 159 +++++++++++++++++++++++++++
 src/core/linux/SDL_progressbar.h     |  30 +++++
 src/video/SDL_video.c                |   6 +
 src/video/wayland/SDL_waylandvideo.c |   4 +
 src/video/x11/SDL_x11video.c         |   4 +
 9 files changed, 215 insertions(+)
 create mode 100644 src/core/linux/SDL_progressbar.c
 create mode 100644 src/core/linux/SDL_progressbar.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index db0eb1cadbd5f..a36d82acdd50c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1736,6 +1736,7 @@ elseif(UNIX AND NOT APPLE AND NOT RISCOS AND NOT HAIKU)
       sdl_sources(
         "${SDL3_SOURCE_DIR}/src/core/linux/SDL_dbus.c"
         "${SDL3_SOURCE_DIR}/src/core/linux/SDL_system_theme.c"
+        "${SDL3_SOURCE_DIR}/src/core/linux/SDL_progressbar.c"
       )
     endif()
 
diff --git a/docs/README-wayland.md b/docs/README-wayland.md
index 75a9b906e106f..a3cd06f78a5fd 100644
--- a/docs/README-wayland.md
+++ b/docs/README-wayland.md
@@ -59,6 +59,15 @@ encounter limitations or behavior that is different from other windowing systems
   `SDL_APP_ID` hint string, the desktop entry file name should match the application ID. For example, if your
   application ID is set to `org.my_org.sdl_app`, the desktop entry file should be named `org.my_org.sdl_app.desktop`.
 
+### The application progress bar can't be set via ```SDL_SetWindowProgressState()``` or ```SDL_SetWindowProgressValue()```
+
+- Only some Desktop Environemnts support the underlying API. Known compatible DEs: Unity, KDE
+- The underlying API requires a desktop entry file, aka a `.desktop` file.
+  Please see the [Desktop Entry Specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/) for
+  more information on the format of this file. Note that if your application manually sets the application ID via the
+  `SDL_APP_ID` hint string, the desktop entry file name should match the application ID. For example, if your
+  application ID is set to `org.my_org.sdl_app`, the desktop entry file should be named `org.my_org.sdl_app.desktop`.
+
 ### Keyboard grabs don't work when running under XWayland
 
 - On GNOME based desktops, the dconf setting `org/gnome/mutter/wayland/xwayland-allow-grabs` must be enabled.
diff --git a/src/core/linux/SDL_dbus.c b/src/core/linux/SDL_dbus.c
index 226a7f3293d56..b61a1cd920e6f 100644
--- a/src/core/linux/SDL_dbus.c
+++ b/src/core/linux/SDL_dbus.c
@@ -68,6 +68,7 @@ static bool LoadDBUSSyms(void)
     SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, const char *, const char *), message_is_signal);
     SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, const char *), message_has_path);
     SDL_DBUS_SYM(DBusMessage *(*)(const char *, const char *, const char *, const char *), message_new_method_call);
+    SDL_DBUS_SYM(DBusMessage *(*)(const char *, const char *, const char *), message_new_signal);
     SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, int, ...), message_append_args);
     SDL_DBUS_SYM(dbus_bool_t (*)(DBusMessage *, int, va_list), message_append_args_valist);
     SDL_DBUS_SYM(void (*)(DBusMessage *, DBusMessageIter *), message_iter_init_append);
diff --git a/src/core/linux/SDL_dbus.h b/src/core/linux/SDL_dbus.h
index 097bc31eb35ab..230b20fded057 100644
--- a/src/core/linux/SDL_dbus.h
+++ b/src/core/linux/SDL_dbus.h
@@ -67,6 +67,7 @@ typedef struct SDL_DBusContext
     dbus_bool_t (*message_is_signal)(DBusMessage *, const char *, const char *);
     dbus_bool_t (*message_has_path)(DBusMessage *, const char *);
     DBusMessage *(*message_new_method_call)(const char *, const char *, const char *, const char *);
+    DBusMessage *(*message_new_signal)(const char *, const char *, const char *);
     dbus_bool_t (*message_append_args)(DBusMessage *, int, ...);
     dbus_bool_t (*message_append_args_valist)(DBusMessage *, int, va_list);
     void (*message_iter_init_append)(DBusMessage *, DBusMessageIter *);
diff --git a/src/core/linux/SDL_progressbar.c b/src/core/linux/SDL_progressbar.c
new file mode 100644
index 0000000000000..e50f8361ca161
--- /dev/null
+++ b/src/core/linux/SDL_progressbar.c
@@ -0,0 +1,159 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_progressbar.h"
+#include "SDL_internal.h"
+
+#include "SDL_dbus.h"
+
+#ifdef SDL_USE_LIBDBUS
+
+#include <unistd.h>
+
+#include "../unix/SDL_appid.h"
+
+#define UnityLauncherAPI_DBUS_INTERFACE "com.canonical.Unity.LauncherEntry"
+#define UnityLauncherAPI_DBUS_SIGNAL    "Update"
+
+static char *GetDBUSObjectPath()
+{
+    char *app_id = SDL_strdup(SDL_GetAppID());
+
+    if (!app_id) {
+        return NULL;
+    }
+
+    // Sanitize exe_name to make it a legal D-Bus path element
+    for (char *p = app_id; *p; ++p) {
+        if (!SDL_isalnum(*p)) {
+            *p = '_';
+        }
+    }
+
+    // Ensure it starts with a letter or underscore
+    if (!SDL_isalpha(app_id[0]) && app_id[0] != '_') {
+        SDL_memmove(app_id + 1, app_id, SDL_strlen(app_id) + 1);
+        app_id[0] = '_';
+    }
+
+    // Create full path
+    char path[1024];
+    SDL_snprintf(path, sizeof(path), "/org/libsdl/%s_%d", app_id, getpid());
+
+    SDL_free(app_id);
+
+    return SDL_strdup(path);
+}
+
+static char *GetAppDesktopPath()
+{
+    const char *desktop_suffix = ".desktop";
+    const char *app_id = SDL_GetAppID();
+    const size_t desktop_path_total_length = SDL_strlen(app_id) + SDL_strlen(desktop_suffix) + 1;
+    char *desktop_path = (char *)SDL_malloc(desktop_path_total_length);
+    if (!desktop_path) {
+        return NULL;
+    }
+    *desktop_path = '\0';
+    SDL_strlcat(desktop_path, app_id, desktop_path_total_length);
+    SDL_strlcat(desktop_path, desktop_suffix, desktop_path_total_length);
+
+    return desktop_path;
+}
+
+static int ShouldShowProgress(SDL_ProgressState progressState)
+{
+    if (progressState == SDL_PROGRESS_STATE_INVALID ||
+        progressState == SDL_PROGRESS_STATE_NONE) {
+        return 0;
+    }
+
+    // Unity LauncherAPI only supports "normal" display of progress
+    return 1;
+}
+
+bool DBUS_ApplyWindowProgress(SDL_VideoDevice *_this, SDL_Window *window)
+{
+    // Signal signature:
+    // signal com.canonical.Unity.LauncherEntry.Update (in s app_uri, in a{sv} properties)
+
+    SDL_DBusContext *dbus = SDL_DBus_GetContext();
+
+    if (!dbus || !dbus->session_conn) {
+        return false;
+    }
+
+    char *objectPath = GetDBUSObjectPath();
+    if (!objectPath) {
+        return false;
+    }
+
+    DBusMessage *msg = dbus->message_new_signal(objectPath, UnityLauncherAPI_DBUS_INTERFACE, UnityLauncherAPI_DBUS_SIGNAL);
+    if (!msg) {
+        SDL_free(objectPath);
+        return false;
+    }
+
+    char *desktop_path = GetAppDesktopPath();
+    if (!desktop_path) {
+        dbus->message_unref(msg);
+        SDL_free(objectPath);
+        return false;
+    }
+
+    const char *progress_visible_str = "progress-visible";
+    const char *progress_str = "progress";
+    int dbus_type_boolean_str = DBUS_TYPE_BOOLEAN;
+    int dbus_type_double_str = DBUS_TYPE_DOUBLE;
+
+    const int progress_visible = ShouldShowProgress(window->progress_state);
+    double progress = (double)window->progress_value;
+
+    DBusMessageIter args, props;
+    dbus->message_iter_init_append(msg, &args);
+    dbus->message_iter_append_basic(&args, DBUS_TYPE_STRING, &desktop_path);   // Setup app_uri paramter
+    dbus->message_iter_open_container(&args, DBUS_TYPE_ARRAY, "{sv}", &props); // Setup properties parameter
+    DBusMessageIter key_it, value_it;
+    // Set progress visible property
+    dbus->message_iter_open_container(&props, DBUS_TYPE_DICT_ENTRY, NULL, &key_it);
+    dbus->message_iter_append_basic(&key_it, DBUS_TYPE_STRING, &progress_visible_str); // Append progress-visible key data
+    dbus->message_iter_open_container(&key_it, DBUS_TYPE_VARIANT, (const char *)&dbus_type_boolean_str, &value_it);
+    dbus->message_iter_append_basic(&value_it, DBUS_TYPE_BOOLEAN, &progress_visible); // Append progress-visible value data
+    dbus->message_iter_close_container(&key_it, &value_it);
+    dbus->message_iter_close_container(&props, &key_it);
+    // Set progress value property
+    dbus->message_iter_open_container(&props, DBUS_TYPE_DICT_ENTRY, NULL, &key_it);
+    dbus->message_iter_append_basic(&key_it, DBUS_TYPE_STRING, &progress_str); // Append progress key data
+    dbus->message_iter_open_container(&key_it, DBUS_TYPE_VARIANT, (const char *)&dbus_type_double_str, &value_it);
+    dbus->message_iter_append_basic(&value_it, DBUS_TYPE_DOUBLE, &progress); // Append progress value data
+    dbus->message_iter_close_container(&key_it, &value_it);
+    dbus->message_iter_close_container(&props, &key_it);
+    dbus->message_iter_close_container(&args, &props);
+
+    dbus->connection_send(dbus->session_conn, msg, NULL);
+
+    SDL_free(desktop_path);
+    dbus->message_unref(msg);
+    SDL_free(objectPath);
+
+    return true;
+}
+
+#endif // SDL_USE_LIBDBUS
diff --git a/src/core/linux/SDL_progressbar.h b/src/core/linux/SDL_progressbar.h
new file mode 100644
index 0000000000000..da9b815f4f12d
--- /dev/null
+++ b/src/core/linux/SDL_progressbar.h
@@ -0,0 +1,30 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_prograssbar_h_
+#define SDL_prograssbar_h_
+
+#include "../../video/SDL_sysvideo.h"
+#include "SDL_internal.h"
+
+extern bool DBUS_ApplyWindowProgress(SDL_VideoDevice *_this, SDL_Window *window);
+
+#endif // SDL_prograssbar_h_
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index c9eb1caa75c40..294fad5d10e51 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -2266,6 +2266,12 @@ static void SDL_FinishWindowCreation(SDL_Window *window, SDL_WindowFlags flags)
             SDL_ShowWindow(window);
         }
     }
+
+#if defined(SDL_PLATFORM_LINUX)
+    // On Linux the progress state is persisted throughout multiple program runs, so reset state on window creation
+    SDL_SetWindowProgressState(window, SDL_PROGRESS_STATE_NONE);
+    SDL_SetWindowProgressValue(window, 0.0f);
+#endif
 }
 
 static bool SDL_ContextNotSupported(const char *name)
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 516419c2818c4..b615bf707905c 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -24,6 +24,7 @@
 #ifdef SDL_VIDEO_DRIVER_WAYLAND
 
 #include "../../core/linux/SDL_system_theme.h"
+#include "../../core/linux/SDL_progressbar.h"
 #include "../../events/SDL_events_c.h"
 
 #include "SDL_waylandclipboard.h"
@@ -629,6 +630,9 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols)
     device->DestroyWindow = Wayland_DestroyWindow;
     device->SetWindowHitTest = Wayland_SetWindowHitTest;
     device->FlashWindow = Wayland_FlashWindow;
+#ifdef SDL_USE_LIBDBUS
+    device->ApplyWindowProgress = DBUS_ApplyWindowProgress;
+#endif // SDL_USE_LIBDBUS
     device->HasScreenKeyboardSupport = Wayland_HasScreenKeyboardSupport;
     device->ShowWindowSystemMenu = Wayland_ShowWindowSystemMenu;
     device->SyncWindow = Wayland_SyncWindow;
diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c
index f371f519d6be0..39fc1e278f9d0 100644
--- a/src/video/x11/SDL_x11video.c
+++ b/src/video/x11/SDL_x11video.c
@@ -25,6 +25,7 @@
 #include <unistd.h> // For getpid() and readlink()
 
 #include "../../core/linux/SDL_system_theme.h"
+#include "../../core/linux/SDL_progressbar.h"
 #include "../../events/SDL_keyboard_c.h"
 #include "../../events/SDL_mouse_c.h"
 #include "../SDL_pixels_c.h"
@@ -204,6 +205,9 @@ static SDL_VideoDevice *X11_CreateDevice(void)
     device->AcceptDragAndDrop = X11_AcceptDragAndDrop;
     device->UpdateWindowShape = X11_UpdateWindowShape;
     device->FlashWindow = X11_FlashWindow;
+#ifdef SDL_USE_LIBDBUS
+    device->ApplyWindowProgress = DBUS_ApplyWindowProgress;
+#endif // SDL_USE_LIBDBUS
     device->ShowWindowSystemMenu = X11_ShowWindowSystemMenu;
     device->SetWindowFocusable = X11_SetWindowFocusable;
     device->SyncWindow = X11_SyncWindow;