SDL_ttf: Added TTF_SetTextPosition() and TTF_GetTextPosition()

From b9b9bf133dadd6677926ecda2d31b5ace838559c Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sat, 19 Oct 2024 12:55:27 -0700
Subject: [PATCH] Added TTF_SetTextPosition() and TTF_GetTextPosition()

This allows you to lay out multiple text objects within a single text area.

Also switched text objects to always wrap on newlines.
---
 examples/showfont.c               |  42 +++++-
 include/SDL3_ttf/SDL_textengine.h |   2 +
 include/SDL3_ttf/SDL_ttf.h        | 145 ++++++++++++++++---
 src/SDL_ttf.c                     | 227 ++++++++++++------------------
 src/SDL_ttf.sym                   |   2 +
 5 files changed, 262 insertions(+), 156 deletions(-)

diff --git a/examples/showfont.c b/examples/showfont.c
index fe804f49..fda33db4 100644
--- a/examples/showfont.c
+++ b/examples/showfont.c
@@ -105,6 +105,16 @@ static void DrawScene(Scene *scene)
     }
 }
 
+static void AdjustTextOffset(TTF_Text *text, int xoffset, int yoffset)
+{
+    int x, y;
+
+    TTF_GetTextPosition(text, &x, &y);
+    x += xoffset;
+    y += yoffset;
+    TTF_SetTextPosition(text, x, y);
+}
+
 static void HandleKeyDown(Scene *scene, SDL_Event *event)
 {
     int style, outline;
@@ -197,16 +207,36 @@ static void HandleKeyDown(Scene *scene, SDL_Event *event)
         TTF_SetFontStyle(scene->font, style);
         break;
 
+    case SDLK_LEFT:
+        if (event->key.mod & SDL_KMOD_CTRL) {
+            AdjustTextOffset(scene->edit->text, -1, 0);
+        }
+        break;
+
+    case SDLK_RIGHT:
+        if (event->key.mod & SDL_KMOD_CTRL) {
+            AdjustTextOffset(scene->edit->text, 1, 0);
+        }
+        break;
+
     case SDLK_UP:
-        /* Increase font size */
-        ptsize = TTF_GetFontSize(scene->font);
-        TTF_SetFontSize(scene->font, ptsize + 1.0f);
+        if (event->key.mod & SDL_KMOD_CTRL) {
+            AdjustTextOffset(scene->edit->text, 0, -1);
+        } else {
+            /* Increase font size */
+            ptsize = TTF_GetFontSize(scene->font);
+            TTF_SetFontSize(scene->font, ptsize + 1.0f);
+        }
         break;
 
     case SDLK_DOWN:
-        /* Decrease font size */
-        ptsize = TTF_GetFontSize(scene->font);
-        TTF_SetFontSize(scene->font, ptsize - 1.0f);
+        if (event->key.mod & SDL_KMOD_CTRL) {
+            AdjustTextOffset(scene->edit->text, 0, 1);
+        } else {
+            /* Decrease font size */
+            ptsize = TTF_GetFontSize(scene->font);
+            TTF_SetFontSize(scene->font, ptsize - 1.0f);
+        }
         break;
 
     case SDLK_ESCAPE:
diff --git a/include/SDL3_ttf/SDL_textengine.h b/include/SDL3_ttf/SDL_textengine.h
index 0a50c704..99222e95 100644
--- a/include/SDL3_ttf/SDL_textengine.h
+++ b/include/SDL3_ttf/SDL_textengine.h
@@ -109,6 +109,8 @@ struct TTF_TextData
 
     bool needs_layout_update;   /**< True if the layout needs to be updated */
     TTF_TextLayout *layout;     /**< Cached layout information, read-only. */
+    int x;                      /**< The x offset of the upper left corner of this text, in pixels, read-only. */
+    int y;                      /**< The y offset of the upper left corner of this text, in pixels, read-only. */
     int w;                      /**< The width of this text, in pixels, read-only. */
     int h;                      /**< The height of this text, in pixels, read-only. */
     int num_ops;                /**< The number of drawing operations to render this text, read-only. */
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index e4dd12c2..19d73539 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -1472,12 +1472,7 @@ extern SDL_DECLSPEC void SDLCALL TTF_DestroyRendererTextEngine(TTF_TextEngine *e
 /**
  * Create a text object from UTF-8 text and a text engine.
  *
- * This will not word-wrap the string; you'll get a surface with a single line
- * of text, as long as the string requires. You can use
- * TTF_CreateText_Wrapped() instead if you need to wrap the output to multiple
- * lines.
- *
- * This will not wrap on newline characters.
+ * This function is equivalent to `TTF_CreateText_Wrapped(engine, font, text, 0)` and will wrap on newline characters.
  *
  * \param engine the text engine to use when creating the text object, may be
  *               NULL.
@@ -1534,7 +1529,10 @@ extern SDL_DECLSPEC TTF_Text * SDLCALL TTF_CreateText_Wrapped(TTF_TextEngine *en
  * \returns a valid property ID on success or 0 on failure; call
  *          SDL_GetError() for more information.
  *
- * \since This function is available since SDL 3.0.0.
+ * \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 SDL_PropertiesID SDLCALL TTF_GetTextProperties(TTF_Text *text);
 
@@ -1545,6 +1543,11 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL TTF_GetTextProperties(TTF_Text *tex
  * \param engine the text engine to use for drawing.
  * \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_SetTextEngine(TTF_Text *text, TTF_TextEngine *engine);
 
@@ -1554,6 +1557,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_SetTextEngine(TTF_Text *text, TTF_TextEngin
  * \param text the TTF_Text to query.
  * \returns the TTF_TextEngine used by the text on success or NULL 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 TTF_TextEngine * SDLCALL TTF_GetTextEngine(TTF_Text *text);
 
@@ -1566,6 +1574,11 @@ extern SDL_DECLSPEC TTF_TextEngine * SDLCALL TTF_GetTextEngine(TTF_Text *text);
  *
  * \param text the TTF_Text to modify.
  * \param font the font to use, may be NULL.
+ *
+ * \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_SetTextFont(TTF_Text *text, TTF_Font *font);
 
@@ -1575,9 +1588,44 @@ extern SDL_DECLSPEC bool SDLCALL TTF_SetTextFont(TTF_Text *text, TTF_Font *font)
  * \param text the TTF_Text to query.
  * \returns the TTF_Font used by the text on success or NULL 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 TTF_Font * SDLCALL TTF_GetTextFont(TTF_Text *text);
 
+/**
+ * Set the position of a text object.
+ *
+ * This can be used to position multiple text objects within a single wrapping text area.
+ *
+ * \param text the TTF_Text to modify.
+ * \param x the x offset of the upper left corner of this text in pixels.
+ * \param y the y offset of the upper left corner of this text in pixels.
+ *
+ * \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_SetTextPosition(TTF_Text *text, int x, int y);
+
+/**
+ * Get the position of a text object.
+ *
+ * \param text the TTF_Text to query.
+ * \param x a pointer filled in with the x offset of the upper left corner of this text in pixels, may be NULL.
+ * \param y a pointer filled in with the y offset of the upper left corner of this text in pixels, may be NULL.
+ *
+ * \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_GetTextPosition(TTF_Text *text, int *x, int *y);
+
 /**
  * Set the UTF-8 text used by a text object.
  *
@@ -1587,6 +1635,11 @@ extern SDL_DECLSPEC TTF_Font * SDLCALL TTF_GetTextFont(TTF_Text *text);
  *               text.
  * \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_SetTextString(TTF_Text *text, const char *string, size_t length);
 
@@ -1603,6 +1656,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_SetTextString(TTF_Text *text, const char *s
  *               text.
  * \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_InsertTextString(TTF_Text *text, int offset, const char *string, size_t length);
 
@@ -1615,6 +1673,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_InsertTextString(TTF_Text *text, int offset
  *               text.
  * \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_AppendTextString(TTF_Text *text, const char *string, size_t length);
 
@@ -1630,6 +1693,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_AppendTextString(TTF_Text *text, const char
  *               remainder of the string.
  * \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_DeleteTextString(TTF_Text *text, int offset, int length);
 
@@ -1637,28 +1705,33 @@ extern SDL_DECLSPEC bool SDLCALL TTF_DeleteTextString(TTF_Text *text, int offset
  * Set whether wrapping is enabled on a text object.
  *
  * \param text the TTF_Text to modify.
- * \param wrap true if wrapping should be enabled, false if it should be
- *             disabled.
  * \param wrapLength the maximum width in pixels, 0 to wrap on newline
- *                   characters, or -1 to leave wrapLength unchanged.
+ *                   characters.
  * \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_SetTextWrapping(TTF_Text *text, bool wrap, int wrapLength);
+extern SDL_DECLSPEC bool SDLCALL TTF_SetTextWrapping(TTF_Text *text, int wrapLength);
 
 /**
  * Get whether wrapping is enabled on a text object.
  *
  * \param text the TTF_Text to query.
- * \param wrap a pointer filled in with true if wrapping is enabled, false if
- *             it is disabled, may be NULL.
  * \param wrapLength a pointer filled in with the maximum width in pixels or 0
- *                   if the text is being wrapped on newline characters, may
- *                   be NULL.
+ *                   if the text is being wrapped on newline characters.
  * \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_GetTextWrapping(TTF_Text *text, bool *wrap, int *wrapLength);
+extern SDL_DECLSPEC bool SDLCALL TTF_GetTextWrapping(TTF_Text *text, int *wrapLength);
 
 /**
  * Get the size of a text object.
@@ -1673,6 +1746,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetTextWrapping(TTF_Text *text, bool *wrap,
  *          NULL.
  * \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_GetTextSize(TTF_Text *text, int *w, int *h);
 
@@ -1727,6 +1805,11 @@ typedef struct TTF_SubString
  *                  offset.
  * \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_GetTextSubString(TTF_Text *text, int offset, TTF_SubString *substring);
 
@@ -1745,6 +1828,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubString(TTF_Text *text, int offset
  *                  offset.
  * \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_GetTextSubStringForLine(TTF_Text *text, int line, TTF_SubString *substring);
 
@@ -1761,6 +1849,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubStringForLine(TTF_Text *text, int
  *          call SDL_GetError() for more information. This is a single
  *          allocation that should be freed with SDL_free() when it is no
  *          longer needed.
+ *
+ * \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 TTF_SubString ** SDLCALL TTF_GetTextSubStringsForRange(TTF_Text *text, int offset, int length, int *count);
 
@@ -1778,6 +1871,11 @@ extern SDL_DECLSPEC TTF_SubString ** SDLCALL TTF_GetTextSubStringsForRange(TTF_T
  *                  the given point.
  * \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_GetTextSubStringForPoint(TTF_Text *text, int x, int y, TTF_SubString *substring);
 
@@ -1791,6 +1889,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubStringForPoint(TTF_Text *text, in
  * \param substring the TTF_SubString to query.
  * \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_GetPreviousTextSubString(TTF_Text *text, const TTF_SubString *substring, TTF_SubString *previous);
 
@@ -1805,6 +1908,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetPreviousTextSubString(TTF_Text *text, co
  * \param next a pointer filled in with the next substring.
  * \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_GetNextTextSubString(TTF_Text *text, const TTF_SubString *substring, TTF_SubString *next);
 
@@ -1818,6 +1926,11 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetNextTextSubString(TTF_Text *text, const
  * \param text the TTF_Text to update.
  * \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_UpdateText(TTF_Text *text);
 
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 9e6ef9c1..8ffe7f55 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -3121,6 +3121,10 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
                 char_count += 1;
             }
             if (cw >= measure_width) {
+                if (cw > measure_width) {
+                    // The last character didn't fit
+                    font->pos_len -= 1;
+                }
                 break;
             }
         }
@@ -3342,7 +3346,7 @@ static bool CharacterIsNewLine(Uint32 c)
     return false;
 }
 
-static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, 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, TTF_Line **lines, int *num_lines, int *w, int *h)
 {
     int width, height;
     int i, numLines = 0, rowHeight;
@@ -3403,51 +3407,61 @@ static bool GetWrappedLines(TTF_Font *font, const char *text, size_t length, int
             strLines[numLines].length = left;
             ++numLines;
 
-            if (!TTF_MeasureString(font, spot, left, wrapLength, &extent, &max_count)) {
+            int measure_width = wrapLength;
+            if (measure_width > 0) {
+                measure_width = SDL_max(measure_width - xoffset, 1);
+            }
+            if (!TTF_MeasureString(font, spot, left, measure_width, &extent, &max_count)) {
                 SDL_SetError("Error measure text");
                 goto done;
             }
 
             if (wrapLength != 0) {
-                if (max_count == 0) {
+                if (max_count == 0 && numLines > 1) {
                     max_count = 1;
                 }
             }
 
-            while (left > 0) {
-                int is_delim;
-                Uint32 c = SDL_StepUTF8((const char **)&spot, &left);
+            if (max_count > 0) {
+                while (left > 0) {
+                    int is_delim;
+                    Uint32 c = SDL_StepUTF8((const char **)&spot, &left);
 
-                if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
-                    continue;
-                }
+                    if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
+                        continue;
+                    }
 
-                char_count += 1;
+                    char_count += 1;
+
+                    // With wrapLength == 0, normal text rendering but newline aware
+                    is_delim = (wrapLength > 0) ? CharacterIsDelimiter(c) : CharacterIsNewLine(c);
 
-                // 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_text = spot;
+                        save_length = left;
+                        // Break, if new line
+                        if (c == '\n' || (c == '\r' && *spot != '\n')) {
+                            break;
+                        }
+                    }
 
-                // Record last delimiter position
-                if (is_delim) {
-                    save_text = spot;
-                    save_length = left;
-                    // Break, if new line
-                    if (c == '\n' || (c == '\r' && *spot != '\n')) {
+                    // Break, if reach the limit
+                    if (char_count == max_count) {
                         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 && left > 0) {
+                    spot = save_text;
+                    left = save_length;
                 }
             }
 
-            // Cut at last delimiter/new lines, otherwise in the middle of the word
-            if (save_text && left > 0) {
-                spot = save_text;
-                left = save_length;
-            }
+            // First line is complete, start the next at offset 0
+            xoffset = 0;
+
         } while (left > 0);
 
         // Trim whitespace from the wrapped lines and newlines from unwrapped lines
@@ -3522,7 +3536,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, wrapLength, NULL, NULL, w, h);
+    return GetWrappedLines(font, text, length, 0, wrapLength, 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)
@@ -3533,7 +3547,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, wrapLength, &strLines, &numLines, &width, &height)) {
+    if (!GetWrappedLines(font, text, length, 0, wrapLength, &strLines, &numLines, &width, &height)) {
         return NULL;
     }
 
@@ -3637,7 +3651,6 @@ SDL_Surface* TTF_RenderText_LCD_Wrapped(TTF_Font *font, const char *text, size_t
 
 struct TTF_TextLayout
 {
-    bool wrap;
     int wrap_length;
     int *lines;
 };
@@ -3649,7 +3662,7 @@ typedef struct TTF_InternalText
     TTF_TextLayout layout;
 } TTF_InternalText;
 
-static TTF_Text *CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length, bool wrap, int wrapLength)
+static TTF_Text *CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length, int wrapLength)
 {
     if (engine && engine->version < sizeof(*engine)) {
         // Update this to handle older versions of this interface
@@ -3672,7 +3685,6 @@ static TTF_Text *CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *
     result->color.a = 1.0f;
     result->internal->needs_layout_update = true;
     result->internal->engine = engine;
-    result->internal->layout->wrap = wrap;
     result->internal->layout->wrap_length = wrapLength;
     if (text && *text) {
         if (length == 0) {
@@ -3696,12 +3708,12 @@ static TTF_Text *CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *
 
 TTF_Text *TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length)
 {
-    return CreateText(engine, font, text, length, false, -1);
+    return CreateText(engine, font, text, length, 0);
 }
 
 TTF_Text *TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length, int wrapLength)
 {
-    return CreateText(engine, font, text, length, true, wrapLength);
+    return CreateText(engine, font, text, length, wrapLength);
 }
 
 static int SDLCALL SortClusters(const void *a, const void *b)
@@ -3796,81 +3808,6 @@ static int CalculateClusterLengths(TTF_Text *text, TTF_SubString *clusters, int
 }
 
 static bool LayoutText(TTF_Text *text)
-{
-    TTF_Font *font = text->internal->font;
-    size_t length = SDL_strlen(text->text);
-    TTF_DrawOperation *ops = NULL;
-    int num_ops = 0, max_ops;
-    TTF_SubString *clusters = NULL, *cluster;
-    int num_clusters = 0;
-    int xstart, ystart, width = 0, height = 0;
-    bool result = false;
-
-    if (!TTF_Size_Internal(font, text->text, length, &width, &height, &xstart, &ystart, NO_MEASUREMENT) || !width) {
-        return true;
-    }
-
-    max_ops = font->pos_len;
-    if (TTF_HANDLE_STYLE_UNDERLINE(font)) {
-        ++max_ops;
-    }
-    if (TTF_HANDLE_STYLE_STRIKETHROUGH(font)) {
-        ++max_ops;
-    }
-
-    ops = (TTF_DrawOperation *)SDL_calloc(max_ops, sizeof(*ops));
-    if (!ops) {
-        goto done;
-    }
-
-    clusters = (TTF_SubString *)SDL_calloc(font->num_clusters + 2, sizeof(*clusters));
-    if (!clusters) {
-        goto done;
-    }
-
-    // Create the text drawing operations
-    if (!Render_Line_TextEngine(font, xstart, ystart, width, height, ops, &num_ops, clusters, &num_clusters, 0, 0)) {
-        goto done;
-    }
-
-    cluster = &clusters[num_clusters++];
-    cluster->flags = TTF_SUBSTRING_LINE_END;
-    cluster->offset = (int)SDL_strlen(text->text);
-
-    cluster = &clusters[num_clusters++];
-    cluster->flags = TTF_SUBSTRING_TEXT_END;
-    cluster->offset = (int)SDL_strlen(text->text);
-
-    // Apply underline or strikethrough style, if needed
-    if (TTF_HANDLE_STYLE_UNDERLINE(font)) {
-        Draw_Line_TextEngine(font, width, height, 0, ystart + font->underline_top_row, width, font->line_thickness, ops, &num_ops);
-    }
-
-    if (TTF_HANDLE_STYLE_STRIKETHROUGH(font)) {
-        Draw_Line_TextEngine(font, width, height, 0, ystart + font->strikethrough_top_row, width, font->line_thickness, ops, &num_ops);
-    }
-
-    num_clusters = CalculateClusterLengths(text, clusters, num_clusters, length, NULL);
-
-    result = true;
-
-done:
-    if (result) {
-        text->num_lines = 1;
-        text->internal->w = width;
-        text->internal->h = height;
-        text->internal->num_ops = num_ops;
-        text->internal->ops = ops;
-        text->internal->num_clusters = num_clusters;
-        text->internal->clusters = clusters;
-    } else {
-        SDL_free(ops);
-        SDL_free(clusters);
-    }
-    return result;
-}
-
-static bool LayoutTextWrapped(TTF_Text *text)
 {
     TTF_Font *font = text->internal->font;
     int wrapLength = text->internal->layout->wrap_length;
@@ -3884,9 +3821,10 @@ static bool LayoutTextWrapped(TTF_Text *text)
     int *lines = NULL;
     bool result = false;
 
-    if (!GetWrappedLines(font, text->text, length, wrapLength, &strLines, &numLines, &width, &height)) {
+    if (!GetWrappedLines(font, text->text, length, text->internal->x, wrapLength, &strLines, &numLines, &width, &height)) {
         return true;
     }
+    height += text->internal->y;
 
     if (TTF_HANDLE_STYLE_UNDERLINE(font)) {
         ++extra_ops;
@@ -3931,6 +3869,8 @@ static bool LayoutTextWrapped(TTF_Text *text)
         // Move to i-th line
         ystart += i * font->lineskip;
 
+        ystart += text->internal->y;
+
         // Control left/right/center align of each bit of text
         if (font->horizontal_align == TTF_HORIZONTAL_ALIGN_RIGHT) {
             xoffset = (width - line_width);
@@ -3941,6 +3881,10 @@ static bool LayoutTextWrapped(TTF_Text *text)
         }
         xoffset = SDL_max(0, xoffset);
 
+        if (i == 0) {
+            xoffset += text->internal->x;
+        }
+
         // Allocate space for the operations on this line
         additional_ops = (font->pos_len + extra_ops);
         new_ops = (TTF_DrawOperation *)SDL_realloc(ops, (max_ops + additional_ops) * sizeof(*new_ops));
@@ -4096,6 +4040,38 @@ TTF_Font *TTF_GetTextFont(TTF_Text *text)
     return text->internal->font;
 }
 
+bool TTF_SetTextPosition(TTF_Text *text, int x, int y)
+{
+    TTF_CHECK_POINTER("text", text, false);
+
+    if (x != text->internal->x || y != text->internal->y) {
+        text->internal->x = x;
+        text->internal->y = y;
+        text->internal->needs_layout_update = true;
+    }
+    return true;
+}
+
+bool TTF_GetTextPosition(TTF_Text *text, int *x, int *y)
+{
+    if (x) {
+        *x = 0;
+    }
+    if (y) {
+        *y = 0;
+    }
+
+    TTF_CHECK_POINTER("text", text, false);
+
+    if (x) {
+        *x = text->internal->x;
+    }
+    if (y) {
+        *y = text->internal->y;
+    }
+    return true;
+}
+
 bool TTF_SetTextString(TTF_Text *text, const char *string, size_t length)
 {
     TTF_CHECK_POINTER("text", text, false);
@@ -4218,38 +4194,27 @@ bool TTF_DeleteTextString(TTF_Text *text, int offset, int length)
     return true;
 }
 
-bool TTF_SetTextWrapping(TTF_Text *text, bool wrap, int wrapLength)
+bool TTF_SetTextWrapping(TTF_Text *text, int wrapLength)
 {
     TTF_CHECK_POINTER("text", text, false);
 
-    if (wrap == text->internal->layout->wrap &&
-        (wrapLength < 0 || wrapLength == text->internal->layout->wrap_length)) {
+    if (wrapLength == text->internal->layout->wrap_length) {
         return true;
     }
 
-    text->internal->layout->wrap = wrap;
-    if (wrapLength >= 0) {
-        text->internal->layout->wrap_length = wrapLength;
-    }
-
+    text->internal->layout->wrap_length = SDL_max(wrapLength, 0);
     text->internal->needs_layout_update = true;
     return true;
 }
 
-bool TTF_GetTextWrapping(TTF_Text *text, bool *wrap, int *wrapLength)
+bool TTF_GetTextWrapping(TTF_Text *text, int *wrapLength)
 {
-    if (wrap) {
-        *wrap = false;
-    }
     if (wrapLength) {
-        *wrapLength = -1;
+        *wrapLength = 0;
     }
 
     TTF_CHECK_POINTER("text", text, false);
 
-    if (wrap) {
-        *wrap = text->internal->layout->wrap;
-    }
     if (wrapLength) {
         *wrapLength = text->internal->layout->wrap_length;
     }
@@ -4545,7 +4510,7 @@ bool TTF_GetTextSubStringForPoint(TTF_Text *text, int x, int y, TTF_SubString *s
 #endif
     const TTF_SubString *closest = NULL;
     int closest_dist = INT_MAX;
-    int wrap_cost = (text->internal->layout->wrap ? 100 : 1);
+    int wrap_cost = 100;
     SDL_Point point = { x, y };
     for (int i = 0; i < text->internal->num_clusters; ++i) {
         const TTF_SubString *cluster = &text->internal->clusters[i];
@@ -4673,14 +4638,8 @@ bool TTF_UpdateText(TTF_Text *text)
         text->internal->h = 0;
 
         if (text->internal->font && text->text) {
-            if (text->internal->layout->wrap) {
-                if (!LayoutTextWrapped(text)) {
-                    return false;
-                }
-            } else {
-                if (!LayoutText(text)) {
-                    return false;
-                }
+            if (!LayoutText(text)) {
+                return false;
             }
         }
 
diff --git a/src/SDL_ttf.sym b/src/SDL_ttf.sym
index dffea6e6..351142b8 100644
--- a/src/SDL_ttf.sym
+++ b/src/SDL_ttf.sym
@@ -45,6 +45,7 @@ SDL3_ttf_0.0.0 {
     TTF_GetStringSizeWrapped;
     TTF_GetTextEngine;
     TTF_GetTextFont;
+    TTF_GetTextPosition;
     TTF_GetTextSize;
     TTF_GetTextSubString;
     TTF_GetTextSubStringForLine;
@@ -81,6 +82,7 @@ SDL3_ttf_0.0.0 {
     TTF_SetFontWrapAlignment;
     TTF_SetTextEngine;
     TTF_SetTextFont;
+    TTF_SetTextPosition;
     TTF_SetTextString;
     TTF_SetTextWrapping;
     TTF_UpdateText;

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