SDL_ttf: Added TTF_GetTextSizeWrapped()

From d1b61f38fc0e521491883f7204294546afb12758 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 27 Sep 2024 00:00:00 -0700
Subject: [PATCH] Added TTF_GetTextSizeWrapped()

Also renamed TTF_SizeText() to TTF_GetTextSize()

Fixes https://github.com/libsdl-org/SDL_ttf/issues/292
---
 build-scripts/SDL_migration.cocci |   5 +
 docs/README-migration.md          |   1 +
 examples/testapp.c                |   2 +-
 include/SDL3_ttf/SDL_ttf.h        |  30 ++++-
 src/SDL_ttf.c                     | 193 +++++++++++++++++++++++++++++-
 src/SDL_ttf.sym                   |   3 +-
 6 files changed, 226 insertions(+), 8 deletions(-)

diff --git a/build-scripts/SDL_migration.cocci b/build-scripts/SDL_migration.cocci
index 9a7c931d..a4135c75 100644
--- a/build-scripts/SDL_migration.cocci
+++ b/build-scripts/SDL_migration.cocci
@@ -160,3 +160,8 @@
 - TTF_SetFontScriptName
 + TTF_SetFontScript
   (...)
+@@
+@@
+- TTF_SizeText
++ TTF_GetTextSize
+  (...)
diff --git a/docs/README-migration.md b/docs/README-migration.md
index bebaa9b7..59edec5c 100644
--- a/docs/README-migration.md
+++ b/docs/README-migration.md
@@ -65,6 +65,7 @@ The following functions have been renamed:
 * TTF_RenderUTF8_Solid_Wrapped() => TTF_RenderText_Solid_Wrapped()
 * TTF_SetFontScriptName() => TTF_SetFontScript()
 * TTF_SetFontWrappedAlign() => TTF_SetFontWrapAlignment()
+* TTF_SizeText() => TTF_GetTextSize()
 * TTF_SizeUTF8() => TTF_SizeText()
 
 The following functions have been removed:
diff --git a/examples/testapp.c b/examples/testapp.c
index 4c92260a..b2e85995 100644
--- a/examples/testapp.c
+++ b/examples/testapp.c
@@ -898,7 +898,7 @@ int main(void)
              }
           }
 #endif
-          if (!TTF_SizeText(font, text, 0, &w, &h)) {
+          if (!TTF_GetTextSize(font, text, 0, &w, &h)) {
              SDL_Log("size failed");
           }
           if (w == 0) {
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index 938e0b02..a6b11868 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -696,12 +696,38 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetGlyphMetrics(TTF_Font *font, Uint32 ch,
  * This will report the width and height, in pixels, of the space that the
  * specified string will take to fully render.
  *
- * This does not need to render the string to do this calculation.
+ * \param font the font to query.
+ * \param text text to calculate, in UTF-8 encoding.
+ * \param length the length of the text, in bytes, or 0 for null terminated
+ *               text.
+ * \param w will be filled with width, in pixels, on return.
+ * \param h will be filled with height, in pixels, on return.
+ * \returns true on success or false on failure; call SDL_GetError() for more
+ *          information.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ *               font.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ */
+extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSize(TTF_Font *font, const char *text, size_t length, int *w, int *h);
+
+/**
+ * Calculate the dimensions of a rendered string of UTF-8 text.
+ *
+ * This will report the width and height, in pixels, of the space that the
+ * specified string will take to fully render.
+ *
+ * Text is wrapped to multiple lines on line endings and on word boundaries if
+ * it extends beyond `wrapLength` in pixels.
+ *
+ * If wrapLength is 0, this function will only wrap on newline characters.
  *
  * \param font the font to query.
  * \param text text to calculate, in UTF-8 encoding.
  * \param length the length of the text, in bytes, or 0 for null terminated
  *               text.
+ * \param wrapLength the maximum width or 0 to wrap on newline characters.
  * \param w will be filled with width, in pixels, on return.
  * \param h will be filled with height, in pixels, on return.
  * \returns true on success or false on failure; call SDL_GetError() for more
@@ -712,7 +738,7 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetGlyphMetrics(TTF_Font *font, Uint32 ch,
  *
  * \since This function is available since SDL_ttf 3.0.0.
  */
-extern SDL_DECLSPEC bool SDLCALL TTF_SizeText(TTF_Font *font, const char *text, size_t length, int *w, int *h);
+extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int wrapLength, int *w, int *h);
 
 /**
  * Calculate how much of a UTF-8 string will fit in a given width.
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 2fa48811..7339dd2e 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -3238,7 +3238,7 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
     return false;
 }
 
-bool TTF_SizeText(TTF_Font *font, const char *text, size_t length, int *w, int *h)
+bool TTF_GetTextSize(TTF_Font *font, const char *text, size_t length, int *w, int *h)
 {
     return TTF_Size_Internal(font, text, length, w, h, NULL, NULL, NO_MEASUREMENT);
 }
@@ -3393,6 +3393,192 @@ static bool CharacterIsNewLine(Uint32 c)
     return false;
 }
 
+bool TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int wrapLength, int *w, int *h)
+{
+    int width, height;
+    Uint8 *utf8_alloc = NULL;
+
+    int i, numLines, rowHeight, lineskip;
+    char **strLines = NULL, *text_cpy;
+    bool result = false;
+
+    TTF_CHECK_INITIALIZED(false);
+    TTF_CHECK_POINTER("font", font, false);
+    TTF_CHECK_POINTER("text", text, false);
+
+    if (!length) {
+        length = SDL_strlen(text);
+    }
+
+    /* Use a copy of the text */
+    utf8_alloc = SDL_stack_alloc(Uint8, length + 1);
+    if (utf8_alloc == NULL) {
+        SDL_OutOfMemory();
+        goto done;
+    }
+    SDL_memcpy(utf8_alloc, text, length);
+    utf8_alloc[length] = 0;
+    text_cpy = (char *)utf8_alloc;
+
+    /* Get the dimensions of the text surface */
+    if (!TTF_GetTextSize(font, text_cpy, length, &width, &height) || !width) {
+        SDL_SetError("Text has zero width");
+        goto done;
+    }
+
+    if (wrapLength < 0) {
+        SDL_InvalidParamError("wrapLength");
+        goto done;
+    }
+
+    numLines = 1;
+
+    if (*text_cpy) {
+        int maxNumLines = 0;
+        size_t textlen = length;
+        numLines = 0;
+
+        do {
+            int extent = 0, max_count = 0, char_count = 0;
+            size_t save_textlen = (size_t)(-1);
+            char *save_text  = NULL;
+
+            if (numLines >= maxNumLines) {
+                char **saved = strLines;
+                if (wrapLength == 0) {
+                    maxNumLines += 32;
+                } else {
+                    maxNumLines += (width / wrapLength) + 1;
+                }
+                strLines = (char **)SDL_realloc(strLines, maxNumLines * sizeof (*strLines));
+                if (strLines == NULL) {
+                    strLines = saved;
+                    goto done;
+                }
+            }
+
+            strLines[numLines++] = text_cpy;
+
+            if (!TTF_MeasureText(font, text_cpy, 0, wrapLength, &extent, &max_count)) {
+                SDL_SetError("Error measure text");
+                goto done;
+            }
+
+            if (wrapLength != 0) {
+                if (max_count == 0) {
+                    max_count = 1;
+                }
+            }
+
+            while (textlen > 0) {
+                int is_delim;
+                Uint32 c = SDL_StepUTF8((const char **)&text_cpy, &textlen);
+
+                if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
+                    continue;
+                }
+
+                char_count += 1;
+
+                /* With wrapLength == 0, normal text rendering but newline aware */
+                is_delim = (wrapLength > 0) ?  CharacterIsDelimiter(c) : CharacterIsNewLine(c);
+
+                /* Record last delimiter position */
+                if (is_delim) {
+                    save_textlen = textlen;
+                    save_text = text_cpy;
+                    /* Break, if new line */
+                    if (c == '\n' || c == '\r') {
+                        *(text_cpy - 1) = '\0';
+                        break;
+                    }
+                }
+
+                /* Break, if reach the limit */
+                if (char_count == max_count) {
+                    break;
+                }
+            }
+
+            /* Cut at last delimiter/new lines, otherwise in the middle of the word */
+            if (save_text && textlen) {
+                text_cpy = save_text;
+                textlen = save_textlen;
+            }
+        } while (textlen > 0);
+    }
+
+    lineskip = TTF_GetFontLineSkip(font);
+    rowHeight = SDL_max(height, lineskip);
+
+    if (wrapLength == 0) {
+        /* Find the max of all line lengths */
+        if (numLines > 1) {
+            width = 0;
+            for (i = 0; i < numLines; i++) {
+                char save_c = 0;
+                int w, h;
+
+                /* Add end-of-line */
+                if (strLines) {
+                    text = strLines[i];
+                    if (i + 1 < numLines) {
+                        save_c = strLines[i + 1][0];
+                        strLines[i + 1][0] = '\0';
+                    }
+                }
+
+                if (TTF_GetTextSize(font, text, 0, &w, &h)) {
+                    width = SDL_max(w, width);
+                }
+
+                /* Remove end-of-line */
+                if (strLines) {
+                    if (i + 1 < numLines) {
+                        strLines[i + 1][0] = save_c;
+                    }
+                }
+            }
+            /* In case there are all newlines */
+            width = SDL_max(width, 1);
+        }
+    } else {
+        if (numLines <= 1 && font->horizontal_align == TTF_HORIZONTAL_ALIGN_LEFT) {
+            /* Don't go above wrapLength if you have only 1 line which hasn't been cut */
+            width = SDL_min((int)wrapLength, width);
+        } else {
+            width = wrapLength;
+        }
+    }
+    height = rowHeight + lineskip * (numLines - 1);
+
+    result = true;
+
+done:
+    if (result) {
+        if (w) {
+            *w = width;
+        }
+        if (h) {
+            *h = height;
+        }
+    } else {
+        if (w) {
+            *w = 0;
+        }
+        if (h) {
+            *h = 0;
+        }
+    }
+    if (strLines) {
+        SDL_free(strLines);
+    }
+    if (utf8_alloc) {
+        SDL_stack_free(utf8_alloc);
+    }
+    return result;
+}
+
 static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text, size_t length, SDL_Color fg, SDL_Color bg, int wrapLength, const render_mode_t render_mode)
 {
     Uint32 color;
@@ -3427,12 +3613,11 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
     text_cpy = (char *)utf8_alloc;
 
     /* Get the dimensions of the text surface */
-    if (!TTF_SizeText(font, text_cpy, length, &width, &height) || !width) {
+    if (!TTF_GetTextSize(font, text_cpy, length, &width, &height) || !width) {
         SDL_SetError("Text has zero width");
         goto failure;
     }
 
-    /* wrapLength is unsigned, but don't allow negative values */
     if (wrapLength < 0) {
         SDL_InvalidParamError("wrapLength");
         goto failure;
@@ -3545,7 +3730,7 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
                     }
                 }
 
-                if (TTF_SizeText(font, text, 0, &w, &h)) {
+                if (TTF_GetTextSize(font, text, 0, &w, &h)) {
                     width = SDL_max(w, width);
                 }
 
diff --git a/src/SDL_ttf.sym b/src/SDL_ttf.sym
index b8bfb910..bb853c13 100644
--- a/src/SDL_ttf.sym
+++ b/src/SDL_ttf.sym
@@ -22,6 +22,8 @@ SDL3_ttf_0.0.0 {
     TTF_GetGlyphMetrics;
     TTF_GetGlyphScript;
     TTF_GetHarfBuzzVersion;
+    TTF_GetTextSize;
+    TTF_GetTextSizeWrapped;
     TTF_Init;
     TTF_MeasureText;
     TTF_OpenFont;
@@ -52,7 +54,6 @@ SDL3_ttf_0.0.0 {
     TTF_SetFontSizeDPI;
     TTF_SetFontStyle;
     TTF_SetFontWrapAlignment;
-    TTF_SizeText;
     TTF_Version;
     TTF_WasInit;
   local: *;