SDL: events: Added SDL_GetEventDescription().

From 22692e308f820dfb9bf6d1af67c89dd1f6174257 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Fri, 25 Apr 2025 13:56:33 -0400
Subject: [PATCH] events: Added SDL_GetEventDescription().

Fixes #12877.
---
 include/SDL3/SDL_events.h         | 32 +++++++++++++++++++
 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           | 51 ++++++++++++++++++++++---------
 5 files changed, 71 insertions(+), 15 deletions(-)

diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index 56a2194b326c1..25c0432503a4c 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -1565,6 +1565,38 @@ extern SDL_DECLSPEC Uint32 SDLCALL SDL_RegisterEvents(int numevents);
  */
 extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowFromEvent(const SDL_Event *event);
 
+/**
+ * Generate a human-readable description of an event.
+ *
+ * This will fill `buf` with a null-terminated string that might look
+ * something like this:
+ *
+ * ```
+ * SDL_EVENT_MOUSE_MOTION (timestamp=1140256324 windowid=2 which=0 state=0 x=492.99 y=139.09 xrel=52 yrel=6)
+ * ```
+ *
+ * The exact format of the string is not guaranteed; it is intended for
+ * logging purposes, to be read by a human, and not parsed by a computer.
+ *
+ * The returned value follows the same rules as SDL_snprintf(): `buf`
+ * will always be NULL-terminated (unless `buflen` is zero), and will be
+ * truncated if `buflen` is too small. The return code is the number of bytes
+ * needed for the complete string, not counting the NULL-terminator, whether
+ * the string was truncated or not. Unlike SDL_snprintf(), though, this
+ * function never returns -1.
+ *
+ * \param event an event to describe. May be NULL.
+ * \param buf the buffer to fill with the description string. May be NULL.
+ * \param buflen the maximum bytes that can be written to `buf`.
+ * \returns number of bytes needed for the full string, not counting the
+ *          null-terminator byte.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.4.0.
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_GetEventDescription(const SDL_Event *event, char *buf, int buflen);
+
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus
 }
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index b602a1a9ca625..a3e47763ea072 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -1252,6 +1252,7 @@ SDL3_0.0.0 {
     SDL_CreateGPURenderer;
     SDL_PutAudioStreamPlanarData;
     SDL_SetAudioIterationCallbacks;
+    SDL_GetEventDescription;
     # 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 bfbce4fa75cb6..4242dab7e97b8 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -1277,3 +1277,4 @@
 #define SDL_CreateGPURenderer SDL_CreateGPURenderer_REAL
 #define SDL_PutAudioStreamPlanarData SDL_PutAudioStreamPlanarData_REAL
 #define SDL_SetAudioIterationCallbacks SDL_SetAudioIterationCallbacks_REAL
+#define SDL_GetEventDescription SDL_GetEventDescription_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index b29428f58b623..b4de5f9493aec 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1285,3 +1285,4 @@ SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetGPUDeviceProperties,(SDL_GPUDevice *a),(
 SDL_DYNAPI_PROC(SDL_Renderer*,SDL_CreateGPURenderer,(SDL_Window *a,SDL_GPUShaderFormat b,SDL_GPUDevice **c),(a,b,c),return)
 SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamPlanarData,(SDL_AudioStream *a,const void * const*b,int c),(a,b,c),return)
 SDL_DYNAPI_PROC(bool,SDL_SetAudioIterationCallbacks,(SDL_AudioDeviceID a,SDL_AudioIterationCallback b,SDL_AudioIterationCallback c,void *d),(a,b,c,d),return)
+SDL_DYNAPI_PROC(int,SDL_GetEventDescription,(const SDL_Event *a,char *b,int c),(a,b,c),return)
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index 349d575015a07..1bac2c2bb8768 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -430,26 +430,18 @@ static void SDLCALL SDL_EventLoggingChanged(void *userdata, const char *name, co
     SDL_EventLoggingVerbosity = (hint && *hint) ? SDL_clamp(SDL_atoi(hint), 0, 3) : 0;
 }
 
-static void SDL_LogEvent(const SDL_Event *event)
+int SDL_GetEventDescription(const SDL_Event *event, char *buf, int buflen)
 {
+    if (!event) {
+        return SDL_snprintf(buf, buflen, "(null)");
+    }
+
     static const char *pen_axisnames[] = { "PRESSURE", "XTILT", "YTILT", "DISTANCE", "ROTATION", "SLIDER", "TANGENTIAL_PRESSURE" };
     SDL_COMPILE_TIME_ASSERT(pen_axisnames_array_matches, SDL_arraysize(pen_axisnames) == SDL_PEN_AXIS_COUNT);
 
     char name[64];
     char details[128];
 
-    // sensor/mouse/pen/finger motion are spammy, ignore these if they aren't demanded.
-    if ((SDL_EventLoggingVerbosity < 2) &&
-        ((event->type == SDL_EVENT_MOUSE_MOTION) ||
-         (event->type == SDL_EVENT_FINGER_MOTION) ||
-         (event->type == SDL_EVENT_PEN_AXIS) ||
-         (event->type == SDL_EVENT_PEN_MOTION) ||
-         (event->type == SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION) ||
-         (event->type == SDL_EVENT_GAMEPAD_SENSOR_UPDATE) ||
-         (event->type == SDL_EVENT_SENSOR_UPDATE))) {
-        return;
-    }
-
 // this is to make (void)SDL_snprintf() calls cleaner.
 #define uint unsigned int
 
@@ -879,12 +871,41 @@ static void SDL_LogEvent(const SDL_Event *event)
         }
         break;
     }
+#undef uint
 
+    int retval = 0;
     if (name[0]) {
-        SDL_Log("SDL EVENT: %s%s", name, details);
+        retval = SDL_snprintf(buf, buflen, "%s%s", name, details);
+    } else if (buf && (buflen > 0)) {
+        *buf = '\0';
     }
+    return retval;
+}
 
-#undef uint
+static void SDL_LogEvent(const SDL_Event *event)
+{
+    if (!event) {
+        return;
+    }
+
+    // sensor/mouse/pen/finger motion are spammy, ignore these if they aren't demanded.
+    if ((SDL_EventLoggingVerbosity < 2) &&
+        ((event->type == SDL_EVENT_MOUSE_MOTION) ||
+         (event->type == SDL_EVENT_FINGER_MOTION) ||
+         (event->type == SDL_EVENT_PEN_AXIS) ||
+         (event->type == SDL_EVENT_PEN_MOTION) ||
+         (event->type == SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION) ||
+         (event->type == SDL_EVENT_GAMEPAD_SENSOR_UPDATE) ||
+         (event->type == SDL_EVENT_SENSOR_UPDATE))) {
+        return;
+    }
+
+    char buf[256];
+    const int rc = SDL_GetEventDescription(event, buf, sizeof (buf));
+    SDL_assert(rc < sizeof (buf));  // if this overflows, we should make `buf` larger, but this is currently larger than the max SDL_GetEventDescription returns.
+    if (buf[0]) {
+        SDL_Log("SDL EVENT: %s", buf);
+    }
 }
 
 void SDL_StopEventLoop(void)