SDL: X11: Handle WM_STATE transitions to detect Withdrawn/Iconic states (#14770)

From 7931321cffa27d4168738ddbe2251966f243afc9 Mon Sep 17 00:00:00 2001
From: Jipok <[EMAIL REDACTED]>
Date: Thu, 8 Jan 2026 00:46:41 +0500
Subject: [PATCH] X11: Handle WM_STATE transitions to detect Withdrawn/Iconic
 states (#14770)

When running SDL3 applications on tiling window managers like i3, moving a window to an invisible workspace does not trigger SDL_WINDOW_MINIMIZED or SDL_WINDOW_HIDDEN. Consequently, the application continues rendering at full speed (VSync dependent), consuming unnecessary GPU/CPU resources even when not visible.

When a workspace is hidden, i3(and possible other tiling WMs) unmaps the container and sets the client window state to WithdrawnState (via the WM_STATE atom). Previously, the SDL3 X11 backend ignored changes to WM_STATE during PropertyNotify events, failing to detect this transition.
---
 src/video/x11/SDL_x11events.c | 30 ++++++++++++++++++++++++++++++
 src/video/x11/SDL_x11video.c  |  1 +
 src/video/x11/SDL_x11video.h  |  1 +
 3 files changed, 32 insertions(+)

diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c
index ebd24ad617a79..cea241edde760 100644
--- a/src/video/x11/SDL_x11events.c
+++ b/src/video/x11/SDL_x11events.c
@@ -2085,6 +2085,36 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent)
             if (changed & SDL_WINDOW_OCCLUDED) {
                 SDL_SendWindowEvent(data->window, (flags & SDL_WINDOW_OCCLUDED) ? SDL_EVENT_WINDOW_OCCLUDED : SDL_EVENT_WINDOW_EXPOSED, 0, 0);
             }
+        } else if (xevent->xproperty.atom == videodata->atoms.WM_STATE) {
+            /* Support for ICCCM-compliant window managers (like i3) that change
+               WM_STATE to WithdrawnState without sending UnmapNotify or updating
+               _NET_WM_STATE when moving windows to invisible workspaces. */
+            Atom type;
+            int format;
+            unsigned long nitems, bytes_after;
+            unsigned char *prop_data = NULL;
+
+            if (X11_XGetWindowProperty(display, data->xwindow, videodata->atoms.WM_STATE,
+                                       0L, 2L, False, videodata->atoms.WM_STATE,
+                                       &type, &format, &nitems, &bytes_after, &prop_data) == Success) {
+                if (nitems > 0) {
+                    // WM_STATE: 0=Withdrawn, 1=Normal, 3=Iconic
+                    Uint32 state = *(Uint32 *)prop_data;
+
+                    if (state == 0 || state == 3) { // Withdrawn or Iconic
+                        if (!(data->window->flags & SDL_WINDOW_MINIMIZED)) {
+                            SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_MINIMIZED, 0, 0);
+                            SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_OCCLUDED, 0, 0);
+                        }
+                    } else if (state == 1) { // NormalState
+                        if (data->window->flags & SDL_WINDOW_MINIMIZED) {
+                            SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_RESTORED, 0, 0);
+                            SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_EXPOSED, 0, 0);
+                        }
+                    }
+                }
+                X11_XFree(prop_data);
+            }
         } else if (xevent->xproperty.atom == videodata->atoms.XKLAVIER_STATE) {
             /* Hack for Ubuntu 12.04 (etc) that doesn't send MappingNotify
                events when the keyboard layout changes (for example,
diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c
index c4f00021f5420..90cb140e53789 100644
--- a/src/video/x11/SDL_x11video.c
+++ b/src/video/x11/SDL_x11video.c
@@ -362,6 +362,7 @@ static bool X11_VideoInit(SDL_VideoDevice *_this)
     GET_ATOM(WM_TAKE_FOCUS);
     GET_ATOM(WM_NAME);
     GET_ATOM(WM_TRANSIENT_FOR);
+    GET_ATOM(WM_STATE);
     GET_ATOM(_NET_WM_STATE);
     GET_ATOM(_NET_WM_STATE_HIDDEN);
     GET_ATOM(_NET_WM_STATE_FOCUSED);
diff --git a/src/video/x11/SDL_x11video.h b/src/video/x11/SDL_x11video.h
index 2dc4d3ab60377..32c372278499c 100644
--- a/src/video/x11/SDL_x11video.h
+++ b/src/video/x11/SDL_x11video.h
@@ -73,6 +73,7 @@ struct SDL_VideoData
         Atom WM_TAKE_FOCUS;
         Atom WM_NAME;
         Atom WM_TRANSIENT_FOR;
+        Atom WM_STATE;
         Atom _NET_WM_STATE;
         Atom _NET_WM_STATE_HIDDEN;
         Atom _NET_WM_STATE_FOCUSED;