SDL_ttf: Removed the need to copy the text while wrapping

From b03582bf49716c5379f38f8c7db7190fd69604be Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 27 Sep 2024 01:32:10 -0700
Subject: [PATCH] Removed the need to copy the text while wrapping

Also trimmed whitespace from wrapped lines.

Fixes https://github.com/libsdl-org/SDL_ttf/issues/350
---
 src/SDL_ttf.c | 217 +++++++++++++++++++-------------------------------
 1 file changed, 80 insertions(+), 137 deletions(-)

diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 5c27a53e..21a9d5b8 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -284,6 +284,12 @@ struct TTF_Font {
     TTF_HorizontalAlignment horizontal_align;
 };
 
+typedef struct
+{
+    const char *text;
+    size_t length;
+} TTF_Line;
+
 /* Tell if SDL_ttf has to handle the style */
 #define TTF_HANDLE_STYLE_BOLD(font)          ((font)->style & TTF_STYLE_BOLD)
 #define TTF_HANDLE_STYLE_ITALIC(font)        ((font)->style & TTF_STYLE_ITALIC)
@@ -3396,10 +3402,8 @@ static bool CharacterIsNewLine(Uint32 c)
 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;
+    int i, numLines = 0, rowHeight, lineskip;
+    TTF_Line *strLines = NULL;
     bool result = false;
 
     TTF_CHECK_INITIALIZED(false);
@@ -3413,51 +3417,43 @@ bool TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int
         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 (!TTF_GetTextSize(font, text, length, &width, &height) || !width) {
+        return SDL_SetError("Text has zero width");
     }
 
-    numLines = 1;
-
-    if (*text_cpy) {
+    if (*text) {
         int maxNumLines = 0;
-        size_t textlen = length;
-        numLines = 0;
+        const char *spot = text;
+        size_t left = length;
 
         do {
             int extent = 0, max_count = 0, char_count = 0;
-            size_t save_textlen = (size_t)(-1);
-            char *save_text  = NULL;
+            const char *save_text = NULL;
+            size_t save_length = (size_t)(-1);
 
             if (numLines >= maxNumLines) {
-                char **saved = strLines;
+                TTF_Line *lines;
                 if (wrapLength == 0) {
                     maxNumLines += 32;
                 } else {
                     maxNumLines += (width / wrapLength) + 1;
                 }
-                strLines = (char **)SDL_realloc(strLines, maxNumLines * sizeof (*strLines));
-                if (strLines == NULL) {
-                    strLines = saved;
+                lines = (TTF_Line *)SDL_realloc(strLines, maxNumLines * sizeof (*strLines));
+                if (lines == NULL) {
                     goto done;
                 }
+                strLines = lines;
             }
 
-            strLines[numLines++] = text_cpy;
+            if (numLines > 0) {
+                strLines[numLines - 1].length = spot - strLines[numLines - 1].text;
+            }
+            strLines[numLines].text = spot;
+            strLines[numLines].length = left;
+            ++numLines;
 
-            if (!TTF_MeasureText(font, text_cpy, 0, wrapLength, &extent, &max_count)) {
+            if (!TTF_MeasureText(font, spot, left, wrapLength, &extent, &max_count)) {
                 SDL_SetError("Error measure text");
                 goto done;
             }
@@ -3468,9 +3464,9 @@ bool TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int
                 }
             }
 
-            while (textlen > 0) {
+            while (left > 0) {
                 int is_delim;
-                Uint32 c = SDL_StepUTF8((const char **)&text_cpy, &textlen);
+                Uint32 c = SDL_StepUTF8((const char **)&spot, &left);
 
                 if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
                     continue;
@@ -3483,11 +3479,10 @@ bool TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int
 
                 /* Record last delimiter position */
                 if (is_delim) {
-                    save_textlen = textlen;
-                    save_text = text_cpy;
+                    save_text = spot;
+                    save_length = left;
                     /* Break, if new line */
                     if (c == '\n' || c == '\r') {
-                        *(text_cpy - 1) = '\0';
                         break;
                     }
                 }
@@ -3499,11 +3494,19 @@ bool TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int
             }
 
             /* Cut at last delimiter/new lines, otherwise in the middle of the word */
-            if (save_text && textlen) {
-                text_cpy = save_text;
-                textlen = save_textlen;
+            if (save_text && left > 0) {
+                spot = save_text;
+                left = save_length;
+            }
+        } while (left > 0);
+
+        /* Trim whitespace from the wrapped lines */
+        for (i = 0; i < (numLines - 1); ++i) {
+            TTF_Line *line = &strLines[i];
+            while (line->length > 0 && CharacterIsDelimiter(line->text[line->length - 1])) {
+                --line->length;
             }
-        } while (textlen > 0);
+        }
     }
 
     lineskip = TTF_GetFontLineSkip(font);
@@ -3514,28 +3517,11 @@ bool TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int
         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)) {
+                if (TTF_GetTextSize(font, strLines[i].text, strLines[i].length, &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);
@@ -3571,9 +3557,6 @@ bool TTF_GetTextSizeWrapped(TTF_Font *font, const char *text, size_t length, int
     if (strLines) {
         SDL_free(strLines);
     }
-    if (utf8_alloc) {
-        SDL_stack_free(utf8_alloc);
-    }
     return result;
 }
 
@@ -3582,10 +3565,9 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
     Uint32 color;
     int width, height;
     SDL_Surface *textbuf = NULL;
-    Uint8 *utf8_alloc = NULL;
 
-    int i, numLines, rowHeight, lineskip;
-    char **strLines = NULL, *text_cpy;
+    int i, numLines = 0, rowHeight, lineskip;
+    TTF_Line *strLines = NULL;
 
     TTF_CHECK_INITIALIZED(NULL);
     TTF_CHECK_POINTER("font", font, NULL);
@@ -3604,18 +3586,8 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
         goto failure;
     }
 
-    /* Use a copy of the text */
-    utf8_alloc = SDL_stack_alloc(Uint8, length + 1);
-    if (utf8_alloc == NULL) {
-        SDL_OutOfMemory();
-        goto failure;
-    }
-    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) {
+    if (!TTF_GetTextSize(font, text, length, &width, &height) || !width) {
         SDL_SetError("Text has zero width");
         goto failure;
     }
@@ -3630,35 +3602,38 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
     }
 #endif
 
-    numLines = 1;
-
-    if (*text_cpy) {
+    if (*text) {
         int maxNumLines = 0;
-        size_t textlen = length;
-        numLines = 0;
+        const char *spot = text;
+        size_t left = length;
 
         do {
             int extent = 0, max_count = 0, char_count = 0;
-            size_t save_textlen = (size_t)(-1);
-            char *save_text  = NULL;
+            const char *save_text = NULL;
+            size_t save_length = (size_t)(-1);
 
             if (numLines >= maxNumLines) {
-                char **saved = strLines;
+                TTF_Line *lines;
                 if (wrapLength == 0) {
                     maxNumLines += 32;
                 } else {
                     maxNumLines += (width / wrapLength) + 1;
                 }
-                strLines = (char **)SDL_realloc(strLines, maxNumLines * sizeof (*strLines));
-                if (strLines == NULL) {
-                    strLines = saved;
+                lines = (TTF_Line *)SDL_realloc(strLines, maxNumLines * sizeof (*strLines));
+                if (lines == NULL) {
                     goto failure;
                 }
+                strLines = lines;
             }
 
-            strLines[numLines++] = text_cpy;
+            if (numLines > 0) {
+                strLines[numLines - 1].length = spot - strLines[numLines - 1].text;
+            }
+            strLines[numLines].text = spot;
+            strLines[numLines].length = left;
+            ++numLines;
 
-            if (!TTF_MeasureText(font, text_cpy, 0, wrapLength, &extent, &max_count)) {
+            if (!TTF_MeasureText(font, spot, left, wrapLength, &extent, &max_count)) {
                 SDL_SetError("Error measure text");
                 goto failure;
             }
@@ -3669,9 +3644,9 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
                 }
             }
 
-            while (textlen > 0) {
+            while (left > 0) {
                 int is_delim;
-                Uint32 c = SDL_StepUTF8((const char **)&text_cpy, &textlen);
+                Uint32 c = SDL_StepUTF8((const char **)&spot, &left);
 
                 if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
                     continue;
@@ -3684,11 +3659,10 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
 
                 /* Record last delimiter position */
                 if (is_delim) {
-                    save_textlen = textlen;
-                    save_text = text_cpy;
+                    save_text = spot;
+                    save_length = left;
                     /* Break, if new line */
                     if (c == '\n' || c == '\r') {
-                        *(text_cpy - 1) = '\0';
                         break;
                     }
                 }
@@ -3700,11 +3674,19 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
             }
 
             /* Cut at last delimiter/new lines, otherwise in the middle of the word */
-            if (save_text && textlen) {
-                text_cpy = save_text;
-                textlen = save_textlen;
+            if (save_text && left > 0) {
+                spot = save_text;
+                left = save_length;
+            }
+        } while (left > 0);
+
+        /* Trim whitespace from the wrapped lines */
+        for (i = 0; i < (numLines - 1); ++i) {
+            TTF_Line *line = &strLines[i];
+            while (line->length > 0 && CharacterIsDelimiter(line->text[line->length - 1])) {
+                --line->length;
             }
-        } while (textlen > 0);
+        }
     }
 
     lineskip = TTF_GetFontLineSkip(font);
@@ -3715,28 +3697,11 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
         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)) {
+                if (TTF_GetTextSize(font, strLines[i].text, strLines[i].length, &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);
@@ -3769,19 +3734,9 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
     /* Render each line */
     for (i = 0; i < numLines; i++) {
         int xstart, ystart, line_width, xoffset;
-        char save_c = 0;
-
-        /* Add end-of-line */
-        if (strLines) {
-            text = strLines[i];
-            if (i + 1 < numLines) {
-                save_c = strLines[i + 1][0];
-                strLines[i + 1][0] = '\0';
-            }
-        }
 
         /* Initialize xstart, ystart and compute positions */
-        if (!TTF_Size_Internal(font, text, 0, &line_width, NULL, &xstart, &ystart, NO_MEASUREMENT)) {
+        if (!TTF_Size_Internal(font, strLines[i].text, strLines[i].length, &line_width, NULL, &xstart, &ystart, NO_MEASUREMENT)) {
             goto failure;
         }
 
@@ -3811,20 +3766,11 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
         if (TTF_HANDLE_STYLE_STRIKETHROUGH(font)) {
             Draw_Line(font, textbuf, xoffset, ystart + font->strikethrough_top_row, line_width, font->line_thickness, color, render_mode);
         }
-        /* Remove end-of-line */
-        if (strLines) {
-            if (i + 1 < numLines) {
-                strLines[i + 1][0] = save_c;
-            }
-        }
     }
 
     if (strLines) {
         SDL_free(strLines);
     }
-    if (utf8_alloc) {
-        SDL_stack_free(utf8_alloc);
-    }
     return textbuf;
 failure:
     if (textbuf) {
@@ -3833,9 +3779,6 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
     if (strLines) {
         SDL_free(strLines);
     }
-    if (utf8_alloc) {
-        SDL_stack_free(utf8_alloc);
-    }
     return NULL;
 }