SDL_ttf: Added TTF_SetTextWrapWhitespaceVisible() and TTF_TextWrapWhitespaceVisible()

From 01ff187c2fca936083fb177c0f43a9a6748a5058 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 19 Oct 2024 19:48:58 -0700
Subject: [PATCH] Added TTF_SetTextWrapWhitespaceVisible() and
 TTF_TextWrapWhitespaceVisible()

When you're editing text, you want whitespace to be visible. When you're rendering it, you normally want it to be eliminated around line breaks. This is the default, to match previous rendering behavior.

Fixes https://github.com/libsdl-org/SDL_ttf/issues/414
---
 examples/editbox.c         |  5 +++-
 include/SDL3_ttf/SDL_ttf.h | 30 ++++++++++++++++++++
 src/SDL_ttf.c              | 58 +++++++++++++++++++++++++++++++++-----
 src/SDL_ttf.sym            |  2 ++
 4 files changed, 87 insertions(+), 8 deletions(-)

diff --git a/examples/editbox.c b/examples/editbox.c
index b2028178..8cd239e9 100644
--- a/examples/editbox.c
+++ b/examples/editbox.c
@@ -218,7 +218,7 @@ static void SaveCandidates(EditBox *edit, const SDL_Event *event)
     }
     *dst = '\0';
 
-    edit->candidates = TTF_CreateText_Wrapped(TTF_GetTextEngine(edit->text), edit->font, candidate_text, 0, 0);
+    edit->candidates = TTF_CreateText(TTF_GetTextEngine(edit->text), edit->font, candidate_text, 0);
     SDL_free(candidate_text);
     if (edit->candidates) {
         SDL_copyp(&edit->candidates->color, &edit->text->color);
@@ -342,6 +342,9 @@ EditBox *EditBox_Create(SDL_Window *window, SDL_Renderer *renderer, TTF_TextEngi
     edit->highlight1 = -1;
     edit->highlight2 = -1;
 
+    /* Show the whitespace when wrapping, so it can be edited */
+    TTF_SetTextWrapWhitespaceVisible(edit->text, true);
+
 #ifdef TEST_SURFACE_ENGINE
     /* Grab the window surface if we want to test the surface text engine.
      * This isn't strictly necessary, we can still use the renderer if it's
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index 1ccadbcc..f78f22fb 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -1737,6 +1737,36 @@ extern SDL_DECLSPEC bool SDLCALL TTF_SetTextWrapping(TTF_Text *text, int wrapLen
  */
 extern SDL_DECLSPEC bool SDLCALL TTF_GetTextWrapping(TTF_Text *text, int *wrapLength);
 
+/**
+ * Set whether whitespace should be visible when wrapping a text object.
+ *
+ * If the whitespace is visible, it will take up space for purposes of alignment and wrapping. This is good for editing, but looks better when centered or aligned if whitespace around line wrapping is hidden. This defaults false.
+ *
+ * \param text the TTF_Text to modify.
+ * \param visible true to show whitespace when wrapping text, false to hide it.
+ * \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
+ *               text.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ */
+extern SDL_DECLSPEC bool SDLCALL TTF_SetTextWrapWhitespaceVisible(TTF_Text *text, bool visible);
+
+/**
+ * Return whether whitespace is shown when wrapping a text object.
+ *
+ * \param text the TTF_Text to query.
+ * \returns true if whitespace is shown when wrapping text, or false otherwise.
+ *
+ * \threadsafety This function should be called on the thread that created the
+ *               text.
+ *
+ * \since This function is available since SDL_ttf 3.0.0.
+ */
+extern SDL_DECLSPEC bool SDLCALL TTF_TextWrapWhitespaceVisible(TTF_Text *text);
+
 /**
  * Get the size of a text object.
  *
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 4add63b2..ec342eec 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -3346,7 +3346,7 @@ static bool CharacterIsNewLine(Uint32 c)
     return false;
 }
 
-static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int xoffset, int wrapLength, TTF_Line **lines, int *num_lines, int *w, int *h)
+static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int xoffset, int wrapLength, bool trim_whitespace, TTF_Line **lines, int *num_lines, int *w, int *h)
 {
     int width, height;
     int i, numLines = 0, rowHeight;
@@ -3400,6 +3400,19 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int
                 strLines = new_lines;
             }
 
+            if (trim_whitespace && spot > text && spot[-1] != '\n') {
+                const char *next_spot = spot;
+                size_t next_left = left;
+                for (;;) {
+                    Uint32 c = SDL_StepUTF8(&next_spot, &next_left);
+                    if (c == 0 || (c != ' ' && c != '\t')) {
+                        break;
+                    }
+                    spot = next_spot;
+                    left = next_left;
+                }
+            }
+
             if (numLines > 0) {
                 strLines[numLines - 1].length = spot - strLines[numLines - 1].text;
             }
@@ -3417,6 +3430,9 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int
             }
 
             if (wrapLength != 0) {
+                // The first line can be empty if we have a text position that's
+                // at the edge of the wrap length, but subsequent lines should have
+                // at least one character per line.
                 if (max_count == 0 && numLines > 1) {
                     max_count = 1;
                 }
@@ -3425,7 +3441,7 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int
             if (max_count > 0) {
                 while (left > 0) {
                     int is_delim;
-                    Uint32 c = SDL_StepUTF8((const char **)&spot, &left);
+                    Uint32 c = SDL_StepUTF8(&spot, &left);
 
                     if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
                         continue;
@@ -3464,18 +3480,24 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int
 
         } while (left > 0);
 
-        // Trim whitespace from the wrapped lines and newlines from unwrapped lines
         for (i = 0; i < numLines; ++i) {
             TTF_Line *line = &strLines[i];
             if (line->length == 0) {
                 continue;
             }
+
+            // The line doesn't include any delimiter that caused it to be wrapped.
             if (CharacterIsNewLine(line->text[line->length - 1])) {
                 --line->length;
                 if (line->text[line->length - 1] == '\r') {
                     --line->length;
                 }
-            } else if (i < (numLines - 1)) {
+            } else if (i < (numLines - 1) &&
+                       CharacterIsDelimiter(line->text[line->length - 1])) {
+                --line->length;
+            }
+
+            if (trim_whitespace) {
                 while (line->length > 0 &&
                        CharacterIsDelimiter(line->text[line->length - 1])) {
                     --line->length;
@@ -3536,7 +3558,7 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int
 
 bool TTF_GetStringSizeWrapped(TTF_Font *font, const char *text, size_t length, int wrapLength, int *w, int *h)
 {
-    return GetWrappedLines(font, text, length, 0, wrapLength, NULL, NULL, w, h);
+    return GetWrappedLines(font, text, length, 0, wrapLength, true, NULL, NULL, w, h);
 }
 
 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)
@@ -3547,7 +3569,7 @@ static SDL_Surface* TTF_Render_Wrapped_Internal(TTF_Font *font, const char *text
     int i, numLines = 0;
     TTF_Line *strLines = NULL;
 
-    if (!GetWrappedLines(font, text, length, 0, wrapLength, &strLines, &numLines, &width, &height)) {
+    if (!GetWrappedLines(font, text, length, 0, wrapLength, true, &strLines, &numLines, &width, &height)) {
         return NULL;
     }
 
@@ -3652,6 +3674,7 @@ SDL_Surface* TTF_RenderText_LCD_Wrapped(TTF_Font *font, const char *text, size_t
 struct TTF_TextLayout
 {
     int wrap_length;
+    bool wrap_whitespace_visible;
     int *lines;
 };
 
@@ -3811,6 +3834,7 @@ static bool LayoutText(TTF_Text *text)
 {
     TTF_Font *font = text->internal->font;
     int wrapLength = text->internal->layout->wrap_length;
+    bool trim_whitespace = !text->internal->layout->wrap_whitespace_visible;
     size_t length = SDL_strlen(text->text);
     int i, width = 0, height = 0, numLines = 0;
     TTF_Line *strLines = NULL;
@@ -3821,7 +3845,7 @@ static bool LayoutText(TTF_Text *text)
     int *lines = NULL;
     bool result = false;
 
-    if (!GetWrappedLines(font, text->text, length, text->internal->x, wrapLength, &strLines, &numLines, &width, &height)) {
+    if (!GetWrappedLines(font, text->text, length, text->internal->x, wrapLength, trim_whitespace, &strLines, &numLines, &width, &height)) {
         return true;
     }
     height += text->internal->y;
@@ -4221,6 +4245,26 @@ bool TTF_GetTextWrapping(TTF_Text *text, int *wrapLength)
     return true;
 }
 
+bool TTF_SetTextWrapWhitespaceVisible(TTF_Text *text, bool visible)
+{
+    TTF_CHECK_POINTER("text", text, false);
+
+    if (visible == text->internal->layout->wrap_whitespace_visible) {
+        return true;
+    }
+
+    text->internal->layout->wrap_whitespace_visible = visible;
+    text->internal->needs_layout_update = true;
+    return true;
+}
+
+bool TTF_GetTextWrapSpaceTrimming(TTF_Text *text)
+{
+    TTF_CHECK_POINTER("text", text, false);
+
+    return text->internal->layout->wrap_whitespace_visible;
+}
+
 bool TTF_GetTextSize(TTF_Text *text, int *w, int *h)
 {
     if (w) {
diff --git a/src/SDL_ttf.sym b/src/SDL_ttf.sym
index 351142b8..c5bb0e1b 100644
--- a/src/SDL_ttf.sym
+++ b/src/SDL_ttf.sym
@@ -84,7 +84,9 @@ SDL3_ttf_0.0.0 {
     TTF_SetTextFont;
     TTF_SetTextPosition;
     TTF_SetTextString;
+    TTF_SetTextWrapWhitespaceVisible;
     TTF_SetTextWrapping;
+    TTF_TextWrapWhitespaceVisible;
     TTF_UpdateText;
     TTF_Version;
     TTF_WasInit;