SDL: Added SDL_SetWindowAspectRatio() and SDL_GetWindowAspectRatio()

From c74886ab001979d9a3395bc608df7d61cbf56daa Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 27 May 2024 15:23:04 -0700
Subject: [PATCH] Added SDL_SetWindowAspectRatio() and
 SDL_GetWindowAspectRatio()

Fixes https://github.com/libsdl-org/SDL/issues/1573
---
 include/SDL3/SDL_test_common.h        |   3 +
 include/SDL3/SDL_video.h              |  55 +++++++++--
 src/SDL_utils.c                       |  39 ++++++++
 src/SDL_utils_c.h                     |   4 +-
 src/dynapi/SDL_dynapi.sym             |   2 +
 src/dynapi/SDL_dynapi_overrides.h     |   2 +
 src/dynapi/SDL_dynapi_procs.h         |   2 +
 src/test/SDL_test_common.c            |  78 ++++++++++------
 src/video/SDL_sysvideo.h              |   3 +
 src/video/SDL_video.c                 |  40 ++++++++
 src/video/cocoa/SDL_cocoawindow.m     |  21 +++++
 src/video/windows/SDL_windowsevents.c | 128 ++++++++++++++++++++++++++
 src/video/x11/SDL_x11video.c          |   1 +
 src/video/x11/SDL_x11window.c         |  21 ++++-
 src/video/x11/SDL_x11window.h         |   1 +
 15 files changed, 364 insertions(+), 36 deletions(-)

diff --git a/include/SDL3/SDL_test_common.h b/include/SDL3/SDL_test_common.h
index d881303e3378c..a7ec4dfca202d 100644
--- a/include/SDL3/SDL_test_common.h
+++ b/include/SDL3/SDL_test_common.h
@@ -76,6 +76,8 @@ typedef struct
     int window_minH;
     int window_maxW;
     int window_maxH;
+    float window_min_aspect;
+    float window_max_aspect;
     int logical_w;
     int logical_h;
     SDL_bool auto_scale_content;
@@ -84,6 +86,7 @@ typedef struct
     float scale;
     int depth;
     float refresh_rate;
+    SDL_bool fill_usable_bounds;
     SDL_bool fullscreen_exclusive;
     SDL_DisplayMode fullscreen_mode;
     int num_windows;
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 22f128f0fa6b3..88e53c896122d 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -1346,9 +1346,6 @@ extern SDL_DECLSPEC int SDLCALL SDL_GetWindowPosition(SDL_Window *window, int *x
 /**
  * Request that the size of a window's client area be set.
  *
- * NULL can safely be passed as the `w` or `h` parameter if the width or
- * height value is not desired.
- *
  * If, at the time of this request, the window in a fixed-size state, such as
  * maximized or fullscreen, the request will be deferred until the window
  * exits this state and becomes resizable again.
@@ -1385,9 +1382,6 @@ extern SDL_DECLSPEC int SDLCALL SDL_SetWindowSize(SDL_Window *window, int w, int
 /**
  * Get the size of a window's client area.
  *
- * NULL can safely be passed as the `w` or `h` parameter if the width or
- * height value is not desired.
- *
  * The window pixel size may differ from its window coordinate size if the
  * window is on a high pixel density display. Use SDL_GetWindowSizeInPixels()
  * or SDL_GetRenderOutputSize() to get the real client area size in pixels.
@@ -1406,6 +1400,55 @@ extern SDL_DECLSPEC int SDLCALL SDL_SetWindowSize(SDL_Window *window, int w, int
  */
 extern SDL_DECLSPEC int SDLCALL SDL_GetWindowSize(SDL_Window *window, int *w, int *h);
 
+/**
+ * Request that the aspect ratio of a window's client area be set.
+ *
+ * The aspect ratio is the ratio of width divided by height, e.g. 2560x1600 would be 1.6. Larger aspect ratios are wider and smaller aspect ratios are narrower.
+ *
+ * If, at the time of this request, the window in a fixed-size state, such as
+ * maximized or fullscreen, the request will be deferred until the window
+ * exits this state and becomes resizable again.
+ *
+ * On some windowing systems, this request is asynchronous and the new window
+ * aspect ratio may not have have been applied immediately upon the return of this
+ * function. If an immediate change is required, call SDL_SyncWindow() to
+ * block until the changes have taken effect.
+ *
+ * When the window size changes, an SDL_EVENT_WINDOW_RESIZED event will be
+ * emitted with the new window dimensions. Note that the new dimensions may
+ * not match the exact aspect ratio requested, as some windowing systems can restrict
+ * the window size in certain scenarios (e.g. constraining the size of the
+ * content area to remain within the usable desktop bounds). Additionally, as
+ * this is just a request, it can be denied by the windowing system.
+ *
+ * \param window the window to change
+ * \param min_aspect the minimum aspect ratio of the window, or 0.0f for no limit
+ * \param max_aspect the maximum aspect ratio of the window, or 0.0f for no limit
+ * \returns 0 on success or a negative error code on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_GetWindowAspectRatio
+ * \sa SDL_SyncWindow
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_SetWindowAspectRatio(SDL_Window *window, float min_aspect, float max_aspect);
+
+/**
+ * Get the size of a window's client area.
+ *
+ * \param window the window to query the width and height from
+ * \param min_aspect a pointer filled in with the minimum aspect ratio of the window, may be NULL
+ * \param max_aspect a pointer filled in with the maximum aspect ratio of the window, may be NULL
+ * \returns 0 on success or a negative error code on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_SetWindowAspectRatio
+ */
+extern SDL_DECLSPEC int SDLCALL SDL_GetWindowAspectRatio(SDL_Window *window, float *min_aspect, float *max_aspect);
+
 /**
  * Get the size of a window's borders (decorations) around the client area.
  *
diff --git a/src/SDL_utils.c b/src/SDL_utils.c
index 581964b0d8a52..e2ce2939d7a19 100644
--- a/src/SDL_utils.c
+++ b/src/SDL_utils.c
@@ -49,6 +49,45 @@ int SDL_powerof2(int x)
     return value;
 }
 
+// Algorithm adapted with thanks from John Cook's blog post:
+// http://www.johndcook.com/blog/2010/10/20/best-rational-approximation
+void SDL_CalculateFraction(float x, int *numerator, int *denominator)
+{
+    const int N = 1000;
+    int a = 0, b = 1;
+    int c = 1, d = 0;
+
+    while (b <= N && d <= N) {
+        float mediant = (float)(a + c) / (b + d);
+        if (x == mediant) {
+            if (b + d <= N) {
+                *numerator = a + c;
+                *denominator = b + d;
+            } else if (d > b) {
+                *numerator = c;
+                *denominator = d;
+            } else {
+                *numerator = a;
+                *denominator = b;
+            }
+            return;
+        } else if (x > mediant) {
+            a = a + c;
+            b = b + d;
+        } else {
+            c = a + c;
+            d = b + d;
+        }
+    }
+    if (b > N) {
+        *numerator = c;
+        *denominator = d;
+    } else {
+        *numerator = a;
+        *denominator = b;
+    }
+}
+
 SDL_bool SDL_endswith(const char *string, const char *suffix)
 {
     size_t string_length = string ? SDL_strlen(string) : 0;
diff --git a/src/SDL_utils_c.h b/src/SDL_utils_c.h
index 98658c3613599..bf0a4fc81ebe6 100644
--- a/src/SDL_utils_c.h
+++ b/src/SDL_utils_c.h
@@ -28,6 +28,8 @@
 /* Return the smallest power of 2 greater than or equal to 'x' */
 extern int SDL_powerof2(int x);
 
-SDL_bool SDL_endswith(const char *string, const char *suffix);
+extern void SDL_CalculateFraction(float x, int *numerator, int *denominator);
+
+extern SDL_bool SDL_endswith(const char *string, const char *suffix);
 
 #endif /* SDL_utils_h_ */
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index d91b6f14b665a..53de3ff3ef217 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -453,6 +453,7 @@ SDL3_0.0.0 {
     SDL_GetUserFolder;
     SDL_GetVersion;
     SDL_GetVideoDriver;
+    SDL_GetWindowAspectRatio;
     SDL_GetWindowBordersSize;
     SDL_GetWindowDisplayScale;
     SDL_GetWindowFlags;
@@ -748,6 +749,7 @@ SDL3_0.0.0 {
     SDL_SetTextureScaleMode;
     SDL_SetThreadPriority;
     SDL_SetWindowAlwaysOnTop;
+    SDL_SetWindowAspectRatio;
     SDL_SetWindowBordered;
     SDL_SetWindowFocusable;
     SDL_SetWindowFullscreen;
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 967b6bfb2649a..f64025b4ea3de 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -478,6 +478,7 @@
 #define SDL_GetUserFolder SDL_GetUserFolder_REAL
 #define SDL_GetVersion SDL_GetVersion_REAL
 #define SDL_GetVideoDriver SDL_GetVideoDriver_REAL
+#define SDL_GetWindowAspectRatio SDL_GetWindowAspectRatio_REAL
 #define SDL_GetWindowBordersSize SDL_GetWindowBordersSize_REAL
 #define SDL_GetWindowDisplayScale SDL_GetWindowDisplayScale_REAL
 #define SDL_GetWindowFlags SDL_GetWindowFlags_REAL
@@ -773,6 +774,7 @@
 #define SDL_SetTextureScaleMode SDL_SetTextureScaleMode_REAL
 #define SDL_SetThreadPriority SDL_SetThreadPriority_REAL
 #define SDL_SetWindowAlwaysOnTop SDL_SetWindowAlwaysOnTop_REAL
+#define SDL_SetWindowAspectRatio SDL_SetWindowAspectRatio_REAL
 #define SDL_SetWindowBordered SDL_SetWindowBordered_REAL
 #define SDL_SetWindowFocusable SDL_SetWindowFocusable_REAL
 #define SDL_SetWindowFullscreen SDL_SetWindowFullscreen_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 251660ff987d4..1cbcb92d155de 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -498,6 +498,7 @@ SDL_DYNAPI_PROC(SDL_Finger**,SDL_GetTouchFingers,(SDL_TouchID a, int *b),(a,b),r
 SDL_DYNAPI_PROC(char*,SDL_GetUserFolder,(SDL_Folder a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_GetVersion,(void),(),return)
 SDL_DYNAPI_PROC(const char*,SDL_GetVideoDriver,(int a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_GetWindowAspectRatio,(SDL_Window *a, float *b, float *c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_GetWindowBordersSize,(SDL_Window *a, int *b, int *c, int *d, int *e),(a,b,c,d,e),return)
 SDL_DYNAPI_PROC(float,SDL_GetWindowDisplayScale,(SDL_Window *a),(a),return)
 SDL_DYNAPI_PROC(SDL_WindowFlags,SDL_GetWindowFlags,(SDL_Window *a),(a),return)
@@ -783,6 +784,7 @@ SDL_DYNAPI_PROC(int,SDL_SetTextureColorModFloat,(SDL_Texture *a, float b, float
 SDL_DYNAPI_PROC(int,SDL_SetTextureScaleMode,(SDL_Texture *a, SDL_ScaleMode b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_SetThreadPriority,(SDL_ThreadPriority a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_SetWindowAlwaysOnTop,(SDL_Window *a, SDL_bool b),(a,b),return)
+SDL_DYNAPI_PROC(int,SDL_SetWindowAspectRatio,(SDL_Window *a, float b, float c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_SetWindowBordered,(SDL_Window *a, SDL_bool b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_SetWindowFocusable,(SDL_Window *a, SDL_bool b),(a,b),return)
 SDL_DYNAPI_PROC(int,SDL_SetWindowFullscreen,(SDL_Window *a, SDL_bool b),(a,b),return)
diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c
index f63db16af5846..25d64352a61f3 100644
--- a/src/test/SDL_test_common.c
+++ b/src/test/SDL_test_common.c
@@ -35,6 +35,7 @@ static const char *common_usage[] = {
 
 static const char *video_usage[] = {
     "[--always-on-top]",
+    "[--aspect min-max]",
     "[--auto-scale-content]",
     "[--center | --position X,Y]",
     "[--confine-cursor X,Y,W,H]",
@@ -373,12 +374,7 @@ int SDLTest_CommonArg(SDLTest_CommonState *state, int index)
             return 2;
         }
         if (SDL_strcasecmp(argv[index], "--usable-bounds") == 0) {
-            /* !!! FIXME: this is a bit of a hack, but I don't want to add a
-               !!! FIXME:  flag to the public structure in 2.0.x */
-            state->window_x = -1;
-            state->window_y = -1;
-            state->window_w = -1;
-            state->window_h = -1;
+            state->fill_usable_bounds = SDL_TRUE;
             return 1;
         }
         if (SDL_strcasecmp(argv[index], "--geometry") == 0) {
@@ -438,6 +434,26 @@ int SDLTest_CommonArg(SDLTest_CommonState *state, int index)
             state->window_maxH = SDL_atoi(h);
             return 2;
         }
+        if (SDL_strcasecmp(argv[index], "--aspect") == 0) {
+            char *min_aspect, *max_aspect;
+            ++index;
+            if (!argv[index]) {
+                return -1;
+            }
+            min_aspect = argv[index];
+            max_aspect = argv[index];
+            while (*max_aspect && *max_aspect != '-') {
+                ++max_aspect;
+            }
+            if (*max_aspect) {
+                *max_aspect++ = '\0';
+            } else {
+                max_aspect = min_aspect;
+            }
+            state->window_min_aspect = SDL_atof(min_aspect);
+            state->window_max_aspect = SDL_atof(max_aspect);
+            return 2;
+        }
         if (SDL_strcasecmp(argv[index], "--logical") == 0) {
             char *w, *h;
             ++index;
@@ -1308,19 +1324,18 @@ SDL_bool SDLTest_CommonInit(SDLTest_CommonState *state)
             SDL_Rect r;
             SDL_PropertiesID props;
 
-            r.x = state->window_x;
-            r.y = state->window_y;
-            r.w = state->window_w;
-            r.h = state->window_h;
-            if (state->auto_scale_content) {
-                float scale = SDL_GetDisplayContentScale(state->displayID);
-                r.w = (int)SDL_ceilf(r.w * scale);
-                r.h = (int)SDL_ceilf(r.h * scale);
-            }
-
-            /* !!! FIXME: hack to make --usable-bounds work for now. */
-            if ((r.x == -1) && (r.y == -1) && (r.w == -1) && (r.h == -1)) {
+            if (state->fill_usable_bounds) {
                 SDL_GetDisplayUsableBounds(state->displayID, &r);
+            } else {
+                r.x = state->window_x;
+                r.y = state->window_y;
+                r.w = state->window_w;
+                r.h = state->window_h;
+                if (state->auto_scale_content) {
+                    float scale = SDL_GetDisplayContentScale(state->displayID);
+                    r.w = (int)SDL_ceilf(r.w * scale);
+                    r.h = (int)SDL_ceilf(r.h * scale);
+                }
             }
 
             if (state->num_windows > 1) {
@@ -1349,6 +1364,9 @@ SDL_bool SDLTest_CommonInit(SDLTest_CommonState *state)
             if (state->window_maxW || state->window_maxH) {
                 SDL_SetWindowMaximumSize(state->windows[i], state->window_maxW, state->window_maxH);
             }
+            if (state->window_min_aspect || state->window_max_aspect) {
+                SDL_SetWindowAspectRatio(state->windows[i], state->window_min_aspect, state->window_max_aspect);
+            }
             SDL_GetWindowSize(state->windows[i], &w, &h);
             if (!(state->window_flags & SDL_WINDOW_RESIZABLE) && (w != r.w || h != r.h)) {
                 SDL_Log("Window requested size %dx%d, got %dx%d\n", r.w, r.h, w, h);
@@ -2376,15 +2394,21 @@ int SDLTest_CommonEventMainCallbacks(SDLTest_CommonState *state, const SDL_Event
             break;
         case SDLK_a:
             if (withControl) {
-                /* Ctrl-A reports absolute mouse position. */
-                float x, y;
-                const SDL_MouseButtonFlags mask = SDL_GetGlobalMouseState(&x, &y);
-                SDL_Log("ABSOLUTE MOUSE: (%g, %g)%s%s%s%s%s\n", x, y,
-                        (mask & SDL_BUTTON_LMASK) ? " [LBUTTON]" : "",
-                        (mask & SDL_BUTTON_MMASK) ? " [MBUTTON]" : "",
-                        (mask & SDL_BUTTON_RMASK) ? " [RBUTTON]" : "",
-                        (mask & SDL_BUTTON_X1MASK) ? " [X2BUTTON]" : "",
-                        (mask & SDL_BUTTON_X2MASK) ? " [X2BUTTON]" : "");
+                /* Ctrl-A toggle aspect ratio */
+                SDL_Window *window = SDL_GetWindowFromID(event->key.windowID);
+                if (window) {
+                    float min_aspect = 0.0f, max_aspect = 0.0f;
+
+                    SDL_GetWindowAspectRatio(window, &min_aspect, &max_aspect);
+                    if (min_aspect > 0.0f || max_aspect > 0.0f) {
+                        min_aspect = 0.0f;
+                        max_aspect = 0.0f;
+                    } else {
+                        min_aspect = 1.0f;
+                        max_aspect = 1.0f;
+                    }
+                    SDL_SetWindowAspectRatio(window, min_aspect, max_aspect);
+                }
             }
             break;
         case SDLK_0:
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 1de2c3d74af29..ba9b6e54d12fc 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -45,6 +45,8 @@ struct SDL_Window
     int w, h;
     int min_w, min_h;
     int max_w, max_h;
+    float min_aspect;
+    float max_aspect;
     int last_pixel_w, last_pixel_h;
     SDL_WindowFlags flags;
     SDL_WindowFlags pending_flags;
@@ -240,6 +242,7 @@ struct SDL_VideoDevice
     void (*SetWindowSize)(SDL_VideoDevice *_this, SDL_Window *window);
     void (*SetWindowMinimumSize)(SDL_VideoDevice *_this, SDL_Window *window);
     void (*SetWindowMaximumSize)(SDL_VideoDevice *_this, SDL_Window *window);
+    void (*SetWindowAspectRatio)(SDL_VideoDevice *_this, SDL_Window *window);
     int (*GetWindowBordersSize)(SDL_VideoDevice *_this, SDL_Window *window, int *top, int *left, int *bottom, int *right);
     void (*GetWindowSizeInPixels)(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h);
     int (*SetWindowOpacity)(SDL_VideoDevice *_this, SDL_Window *window, float opacity);
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 7f30b46ffa1b6..1d4911941e654 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -2504,6 +2504,10 @@ int SDL_RecreateWindow(SDL_Window *window, SDL_WindowFlags flags)
         _this->SetWindowMaximumSize(_this, window);
     }
 
+    if (_this->SetWindowAspectRatio && (window->min_aspect > 0.0f || window->max_aspect > 0.0f)) {
+        _this->SetWindowAspectRatio(_this, window);
+    }
+
     if (window->hit_test) {
         _this->SetWindowHitTest(window, SDL_TRUE);
     }
@@ -2776,6 +2780,7 @@ int SDL_SetWindowAlwaysOnTop(SDL_Window *window, SDL_bool on_top)
 int SDL_SetWindowSize(SDL_Window *window, int w, int h)
 {
     CHECK_WINDOW_MAGIC(window, -1);
+
     if (w <= 0) {
         return SDL_InvalidParamError("w");
     }
@@ -2783,6 +2788,16 @@ int SDL_SetWindowSize(SDL_Window *window, int w, int h)
         return SDL_InvalidParamError("h");
     }
 
+    /* It is possible for the aspect ratio contraints to not satisfy the size constraints. */
+    /* The size constraints will override the aspect ratio contraints so we will apply the */
+    /* the aspect ratio constraints first */
+    float new_aspect = w / (float)h;
+    if (window->max_aspect > 0.0f && new_aspect > window->max_aspect) {
+        w = (int)SDL_roundf(h * window->max_aspect);
+    } else if (window->min_aspect > 0.0f && new_aspect < window->min_aspect) {
+        h = (int)SDL_roundf(w / window->min_aspect);
+    }
+
     /* Make sure we don't exceed any window size limits */
     if (window->min_w && w < window->min_w) {
         w = window->min_w;
@@ -2821,6 +2836,31 @@ int SDL_GetWindowSize(SDL_Window *window, int *w, int *h)
     return 0;
 }
 
+int SDL_SetWindowAspectRatio(SDL_Window *window, float min_aspect, float max_aspect)
+{
+    CHECK_WINDOW_MAGIC(window, -1);
+
+    window->min_aspect = min_aspect;
+    window->max_aspect = max_aspect;
+    if (_this->SetWindowAspectRatio) {
+        _this->SetWindowAspectRatio(_this, window);
+    }
+    return SDL_SetWindowSize(window, window->floating.w, window->floating.h);
+}
+
+int SDL_GetWindowAspectRatio(SDL_Window *window, float *min_aspect, float *max_aspect)
+{
+    CHECK_WINDOW_MAGIC(window, -1);
+
+    if (min_aspect) {
+        *min_aspect = window->min_aspect;
+    }
+    if (max_aspect) {
+        *max_aspect = window->max_aspect;
+    }
+    return 0;
+}
+
 int SDL_GetWindowBordersSize(SDL_Window *window, int *top, int *left, int *bottom, int *right)
 {
     int dummy = 0;
diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index 067c23af4c0ca..005f75ed74f14 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -986,6 +986,27 @@ - (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize
         _data.checking_zoom = NO;
     }
 
+    if (window->min_aspect > 0.0f || window->max_aspect > 0.0f) {
+        NSWindow *nswindow = _data.nswindow;
+        NSRect newContentRect = [nswindow contentRectForFrameRect:NSMakeRect(0, 0, frameSize.width, frameSize.height)];
+        NSSize newSize = newContentRect.size;
+        CGFloat minAspectRatio = window->min_aspect;
+        CGFloat maxAspectRatio = window->max_aspect;
+        CGFloat aspectRatio;
+
+        if (newSize.height > 0) {
+            aspectRatio = newSize.width / newSize.height;
+
+            if (maxAspectRatio > 0.0f && aspectRatio > maxAspectRatio) {
+                newSize.width = (int)SDL_roundf(newSize.height * maxAspectRatio);
+            } else if (minAspectRatio > 0.0f && aspectRatio < minAspectRatio) {
+                newSize.height = (int)SDL_roundf(newSize.width / minAspectRatio);
+            }
+
+            NSRect newFrameRect = [nswindow frameRectForContentRect:NSMakeRect(0, 0, newSize.width, newSize.height)];
+            frameSize = newFrameRect.size;
+        }
+    }
     return frameSize;
 }
 
diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index 3aff7ed0dbaaa..b4dda500b1034 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -1479,6 +1479,134 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara
         KillTimer(hwnd, (UINT_PTR)SDL_IterateMainCallbacks);
     } break;
 
+    case WM_SIZING:
+        {
+            Uint32 edge = wParam;
+            RECT* dragRect = (RECT*)lParam;
+            RECT clientDragRect = *dragRect;
+            SDL_bool lock_aspect_ratio = (data->window->max_aspect == data->window->min_aspect) ? SDL_TRUE : SDL_FALSE;
+            RECT rc;
+            LONG w, h;
+            float new_aspect;
+
+            /* if aspect ratio constraints are not enabled then skip this message */
+            if (data->window->min_aspect <= 0 && data->window->max_aspect <= 0) {
+                break;
+            }
+
+            /* unadjust the dragRect from the window rect to the client rect */
+            SetRectEmpty(&rc);
+            if (!AdjustWindowRectEx(&rc, GetWindowStyle(hwnd), GetMenu(hwnd) != NULL, GetWindowExStyle(hwnd))) {
+                break;
+            }
+
+            clientDragRect.left -= rc.left;
+            clientDragRect.top -= rc.top;
+            clientDragRect.right -= rc.right;
+            clientDragRect.bottom -= rc.bottom;
+
+            w = clientDragRect.right - clientDragRect.left;
+            h = clientDragRect.bottom - clientDragRect.top;
+            new_aspect = w / (float)h;
+
+            /* handle the special case in which the min ar and max ar are the same so the window can size symmetrically */
+            if (lock_aspect_ratio) {
+                switch (edge) {
+                case WMSZ_LEFT:
+                case WMSZ_RIGHT:
+                    h = (int)(w / data->window->max_aspect);
+                    break;
+                default:
+                    /* resizing via corners or top or bottom */
+                    w = (int)(h*data->window->max_aspect);
+                    break;
+                }
+            } else {
+                switch (edge) {
+                case WMSZ_LEFT:
+                case WMSZ_RIGHT:
+                    if (data->window->max_aspect > 0.0f && new_aspect > data->window->max_aspect) {
+                        w = (int)SDL_roundf(h * data->window->max_aspect);
+                    } else if (data->window->min_aspect > 0.0f && new_aspect < data->window->min_aspect) {
+                        w = (int)SDL_roundf(h * data->window->min_aspect);
+                    }
+                    break;
+                case WMSZ_TOP:
+                case WMSZ_BOTTOM:
+                    if (data->window->min_aspect > 0.0f && new_aspect < data->window->min_aspect) {
+                        h = (int)SDL_roundf(w / data->window->min_aspect);
+                    } else if (data->window->max_aspect > 0.0f && new_aspect > data->window->max_aspect) {
+                        h = (int)SDL_roundf(w / data->window->max_aspect);
+                    }
+                    break;
+
+                default:
+                    /* resizing via corners */
+                    if (data->window->max_aspect > 0.0f && new_aspect > data->window->max_aspect) {
+                        w = (int)SDL_roundf(h * data->window->max_aspect);
+                    } else if (data->window->min_aspect > 0.0f && new_aspect < data->window->min_aspect) {
+                        h = (int)SDL_roundf(w / data->window->min_aspect);
+                    }
+                    break;
+                }
+            }
+
+            switch (edge) {
+            case WMSZ_LEFT:
+                clientDragRect.left = clientDragRect.right - w;
+                if (lock_aspect_ratio) {
+                    clientDragRect.top = (clientDragRect.bottom + clientDragRect.top - h) / 2;
+                }
+                clientDragRect.bottom = h + clientDragRect.top;
+                break;
+            case WMSZ_BOTTOMLEFT:
+                clientDragRect.left = clientDragRect.right - w;
+                clientDragRect.bottom = h + clientDragRect.top;
+                break;
+            case WMSZ_RIGHT:
+                clientDragRect.right = w + clientDragRect.left;
+                if (lock_aspect_ratio) {
+                    clientDragRect.top = (clientDragRect.bottom + clientDragRect.top - h) / 2;
+                }
+                clientDragRect.bottom = h + clientDragRect.top;
+                break;
+            case WMSZ_TOPRIGHT:
+                clientDragRect.right = w + clientDragRect.left;
+                clientDragRect.top = clientDragRect.bottom - h;
+                break;
+            case WMSZ_TOP:
+                if (lock_aspect_ratio) {
+                    clientDragRect.left = (clientDragRect.right + clientDragRect.left - w) / 2;
+                }
+                clientDragRect.right = w + clientDragRect.left;
+                clientDragRect.top = clientDragRect.bottom - h;
+                break;
+            case WMSZ_TOPLEFT:
+                clientDragRect.left = clientDragRect.right - w;
+                clientDragRect.top = clientDragRect.bottom - h;
+                break;
+            case WMSZ_BOTTOM:
+                if (lock_aspect_ratio) {
+                    clientDragRect.left = (clientDragRect.right + clientDragRect.left - w) / 2;
+                }
+                clientDragRect.right = w + clientDragRect.left;
+                clientDragRect.bottom = h + clientDragRect.top;
+                break;
+            case WMSZ_BOTTOMRIGHT:
+                clientDragRect.right = w + clientDragRect.left;
+                clientDragRect.bottom = h + clientDragRect.top;
+                break;
+            }
+
+            /* convert the client rect to a window rect */
+            if (!AdjustWindowRectEx(&clientDragRect, GetWindowStyle(hwnd), GetMenu(hwnd) != NULL, GetWindowExStyle(hwnd))) {
+                break;
+            }
+
+            *dragRect = clientDragRect;
+        }
+        break;
+
     case WM_SETCURSOR:
     {
         Uint16 hittest;
diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c
index 952c501d97e74..4a2692faa4cb0 100644
--- a/src/video/x11/SDL_x11video.c
+++ b/src/video/x11/SDL_x11video.c
@@ -194,6 +194,7 @@ static SDL_VideoDevice *X11_CreateDevice(void)
     device->SetWindowSize = X11_SetWindowSize;
     device->SetWindowMinimumSize = X11_SetWindowMinimumSize;
     device->SetWindowMaximumSize = X11_SetWindowMaximumSize;
+    device->SetWindowAspectRatio = X11_SetWindowAspectRatio;
     device->GetWindowBordersSize = X11_GetWindowBordersSize;
     device->SetWindowOpacity = X11_SetWindowOpacity;
     device->SetWindowModalFor = X11_SetWindowModalFor;
diff --git a/src/video/x11/SDL_x11window.c b/src/video/x11/SDL_x11window.c
index e2dca2d5d083b..02737c6480d03 100644
--- a/src/video/x11/SDL_x11window.c
+++ b/src/video/x11/SDL_x11window.c
@@ -28,6 +28,7 @@
 #include "../../events/SDL_mouse_c.h"
 #include "../../events/SDL_events_c.h"
 #include "../../core/unix/SDL_appid.h"
+#include "../../SDL_utils_c.h"
 
 #include "SDL_x11video.h"
 #include "SDL_x11mouse.h"
@@ -1086,7 +1087,7 @@ void X11_SetWindowMinMax(SDL_Window *window, SDL_bool use_current)
     long hint_flags = 0;
 
     X11_XGetWMNormalHints(display, data->xwindow, sizehints, &hint_flags);
-    sizehints->flags &= ~(PMinSize | PMaxSize);
+    sizehints->flags &= ~(PMinSize | PMaxSize | PAspect);
 
     if (data->window->flags & SDL_WINDOW_RESIZABLE) {
         if (data->window->min_w || data->window->min_h) {
@@ -1099,6 +1100,11 @@ void X11_SetWindowMinMax(SDL_Window *window, SDL_bool use_current)
             sizehints->max_width = data->window->max_w;
             sizehints->max_height = data->window->max_h;
         }
+        if (data->window->min_aspect > 0.0f || data->window->max_aspect > 0.0f) {
+            sizehints->flags |= PAspect;
+            SDL_CalculateFraction(data->window->min_aspect, &sizehints->min_aspect.x, &sizehints->min_aspect.y);
+            SDL_CalculateFraction(data->window->max_aspect, &sizehints->max_aspect.x, &sizehints->max_aspect.y);
+        }
     } else {
         /* Set the min/max to the same values to make the window non-resizable */
         sizehints->flags |= PMinSize | PMaxSize;
@@ -1132,6 +1138,17 @@ void X11_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window)
     }
 }
 
+void X11_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window)
+{
+    if (window->driverdata->pending_operation & X11_PENDING_OP_FULLSCREEN) {
+        X11_SyncWindow(_this, window);
+    }
+
+    if (!(window->flags & SDL_WINDOW_FULLSCREEN)) {
+        X11_SetWindowMinMax(window, SDL_TRUE);
+    }
+}
+
 void X11_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window)
 {
     SDL_WindowData *data = window->driverdata;
@@ -1645,7 +1662,7 @@ static int X11_SetWindowFullscreenViaWM(SDL_VideoDevice *_this, SDL_Window *wind
             long flags = 0;
             X11_XGetWMNormalHints(display, data->xwindow, sizehints, &flags);
             /* we are going fullscreen so turn the flags off */
-            sizehints->flags &= ~(PMinSize | PMaxSize);
+            sizehints->flags &= ~(PMinSize | PMaxSize | PAspect);
             X11_XSetWMNormalHints(display, data->xwindow, sizehints);
             X11_XFree(sizehints);
         }
diff --git a/src/video/x11/SDL_x11window.h b/src/video/x11/SDL_x11window.h
index 58b2a253d34a0..204c7e4af5b4f 100644
--- a/src/video/x11/SDL_x11window.h
+++ b/src/video/x11/SDL_x11window.h
@@ -114,6 +114,7 @@ extern int X11_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Sur
 extern int X11_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window);
 extern void X11_SetWindowMinimumSize(SDL_VideoDevice *_this, SDL_Window *window);
 extern void X11_SetWindowMaximumSize(SDL_VideoDevice *_this, SDL_Window *window);
+extern void X11_SetWindowAspectRatio(SDL_VideoDevice *_this, SDL_Window *window);
 extern int X11_GetWindowBordersSize(SDL_VideoDevice *_this, SDL_Window *window, int *top, int *left, int *bottom, int *right);
 extern int X11_SetWindowOpacity(SDL_VideoDevice *_this, SDL_Window *window, float opacity);
 extern int X11_SetWindowModalFor(SDL_VideoDevice *_this, SDL_Window *modal_window, SDL_Window 

(Patch may be truncated, please check the link at the top of this post.)