SDL_ttf: Added string offset information to TTF_Text

From dc50444941241095b406dbbf7395ce146187f79e Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Wed, 2 Oct 2024 07:29:27 -0700
Subject: [PATCH] Added string offset information to TTF_Text

Also keep the text string around and drawing operations around and allow creating TTF_Text with no engine, for measurement or external rendering use.
---
 include/SDL3_ttf/SDL_textengine.h |  37 ++++++----
 include/SDL3_ttf/SDL_ttf.h        |   6 +-
 src/SDL_renderer_textengine.c     |  18 +++--
 src/SDL_surface_textengine.c      |  10 +--
 src/SDL_ttf.c                     | 113 +++++++++++++++++++++---------
 5 files changed, 129 insertions(+), 55 deletions(-)

diff --git a/include/SDL3_ttf/SDL_textengine.h b/include/SDL3_ttf/SDL_textengine.h
index 96def874..f975e40e 100644
--- a/include/SDL3_ttf/SDL_textengine.h
+++ b/include/SDL3_ttf/SDL_textengine.h
@@ -38,14 +38,6 @@
 extern "C" {
 #endif
 
-/* Private data in TTF_Text, available to implementations */
-struct TTF_TextData
-{
-    TTF_TextEngine *engine; /**< The engine used to create this text, read-only. */
-    SDL_PropertiesID props; /**< Custom properties associated with this text, read-write. */
-    void *textrep;          /**< The implementation-specific representation of this text */
-};
-
 /**
  * A font atlas draw command.
  *
@@ -81,6 +73,12 @@ typedef struct TTF_FillOperation
 typedef struct TTF_CopyOperation
 {
     TTF_DrawCommand cmd;    /**< TTF_DRAW_COMMAND_COPY */
+    int text_offset;        /**< The offset in the text corresponding to this glyph.
+                                 There may be multiple glyphs with the same text offset
+                                 and the next text offset might be several Unicode codepoints
+                                 later. In this case the glyphs and codepoints are grouped
+                                 together and the group bounding box is the union of the dst
+                                 rectangles for the corresponding glyphs. */
     Uint32 glyph_index;     /**< The glyph index of the glyph to be drawn, can be passed to TTF_GetGlyphForIndex() */
     SDL_Rect src;           /**< The area within the glyph to be drawn */
     SDL_Rect dst;           /**< The drawing coordinates of the glyph, in pixels. The x coordinate is relative to the left side of the text area, going right, and the y coordinate is relative to the top side of the text area, going down. */
@@ -99,6 +97,22 @@ typedef union TTF_DrawOperation
     TTF_CopyOperation copy;
 } TTF_DrawOperation;
 
+
+/* Private data in TTF_Text, to assist in text measurement and layout */
+typedef struct TTF_TextLayout TTF_TextLayout;
+
+
+/* Private data in TTF_Text, available to implementations */
+struct TTF_TextData
+{
+    SDL_PropertiesID props; /**< Custom properties associated with this text, read-only. This field is created as-needed using TTF_GetTextProperties() and the properties may be then set and read normally */
+    int num_ops;            /**< The number of drawing operations to render this text, read-only. */
+    TTF_DrawOperation *ops; /**< The drawing operations used to render this text, read-only. */
+    TTF_TextLayout *layout; /**< Cached layout information, read-only */
+    TTF_TextEngine *engine; /**< The engine used to create this text, read-only. */
+    void *engine_text;      /**< The implementation-specific representation of this text */
+};
+
 /**
  * A text engine interface.
  *
@@ -116,15 +130,14 @@ struct TTF_TextEngine
 
     /* Create a text representation from draw instructions.
      *
-     * All fields of `text` except `internal` will already be filled out.
+     * All fields of `text` except `internal->engine_text` will already be filled out.
      *
      * \param userdata the userdata pointer in this interface.
      * \param font the font being used.
      * \param font_generation the unique ID of the font generation being used. This changes whenever the font changes size or style and needs new glyphs, and is unique across all fonts.
-     * \param ops the text drawing operations
-     * \param num_ops the number of text drawing operations
+     * \param text the text object being created.
      */
-    bool (SDLCALL *CreateText)(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text, TTF_DrawOperation *ops, int num_ops);
+    bool (SDLCALL *CreateText)(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text);
 
     /**
      * Destroy a text representation.
diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h
index 36967e84..49b15f01 100644
--- a/include/SDL3_ttf/SDL_ttf.h
+++ b/include/SDL3_ttf/SDL_ttf.h
@@ -1244,7 +1244,7 @@ typedef struct TTF_TextData TTF_TextData;
  */
 typedef struct TTF_Text
 {
-    char *label;            /**< A label that you can allocate with SDL_strdup() for debugging purposes, and will be automatically freed in TTF_DestroyText(). */
+    char *text;             /**< A copy of the text used to create this text object, useful for layout and debugging. This will be freed automatically when the object is destroyed. */
     int w;                  /**< The width of this text, in pixels, read-only. */
     int h;                  /**< The height of this text, in pixels, read-only. */
     SDL_FColor color;       /**< The color of the text, read-write. You can change this anytime. */
@@ -1384,7 +1384,7 @@ extern SDL_DECLSPEC void SDLCALL TTF_DestroyRendererTextEngine(TTF_TextEngine *e
  *
  * This will not wrap on newline characters.
  *
- * \param engine the text engine to use when creating the text object.
+ * \param engine the text engine to use when creating the text object, may be NULL.
  * \param font the font to render with.
  * \param text the text to use, in UTF-8 encoding.
  * \param length the length of the text, in bytes, or 0 for null terminated
@@ -1410,7 +1410,7 @@ extern SDL_DECLSPEC TTF_Text * SDLCALL TTF_CreateText(TTF_TextEngine *engine, TT
  *
  * If wrapLength is 0, this function will only wrap on newline characters.
  *
- * \param engine the text engine to use when creating the text object.
+ * \param engine the text engine to use when creating the text object, may be NULL.
  * \param font the font to render with.
  * \param text the text to use, in UTF-8 encoding.
  * \param length the length of the text, in bytes, or 0 for null terminated
diff --git a/src/SDL_renderer_textengine.c b/src/SDL_renderer_textengine.c
index e0ff1c39..64234047 100644
--- a/src/SDL_renderer_textengine.c
+++ b/src/SDL_renderer_textengine.c
@@ -781,8 +781,10 @@ static TTF_RendererTextEngineData *CreateEngineData(SDL_Renderer *renderer)
     return data;
 }
 
-static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text, TTF_DrawOperation *ops, int num_ops)
+static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text)
 {
+    int num_ops = text->internal->num_ops;
+    TTF_DrawOperation *ops;
     TTF_RendererTextEngineData *enginedata = (TTF_RendererTextEngineData *)userdata;
     TTF_RendererTextEngineFontData *fontdata;
     TTF_RendererTextEngineTextData *data;
@@ -797,17 +799,25 @@ static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_gener
         fontdata->generation = font_generation;
     }
 
+    // Make a sortable copy of the draw operations
+    ops = (TTF_DrawOperation *)SDL_malloc(num_ops * sizeof(*ops));
+    if (!ops) {
+        return false;
+    }
+    SDL_memcpy(ops, text->internal->ops, num_ops * sizeof(*ops));
+
     data = CreateTextData(enginedata, fontdata, ops, num_ops);
+    SDL_free(ops);
     if (!data) {
         return false;
     }
-    text->internal->textrep = data;
+    text->internal->engine_text = data;
     return true;
 }
 
 static void SDLCALL DestroyText(void *userdata, TTF_Text *text)
 {
-    TTF_RendererTextEngineTextData *data = (TTF_RendererTextEngineTextData *)text->internal->textrep;
+    TTF_RendererTextEngineTextData *data = (TTF_RendererTextEngineTextData *)text->internal->engine_text;
 
     (void)userdata;
     DestroyTextData(data);
@@ -846,7 +856,7 @@ bool TTF_DrawRendererText(TTF_Text *text, float x, float y)
     }
 
     renderer = ((TTF_RendererTextEngineData *)text->internal->engine->userdata)->renderer;
-    data = (TTF_RendererTextEngineTextData *)text->internal->textrep;
+    data = (TTF_RendererTextEngineTextData *)text->internal->engine_text;
     AtlasDrawSequence *sequence = data->draw_sequence;
     while (sequence) {
         float *position = sequence->positions;
diff --git a/src/SDL_surface_textengine.c b/src/SDL_surface_textengine.c
index df4f0212..7952cd8b 100644
--- a/src/SDL_surface_textengine.c
+++ b/src/SDL_surface_textengine.c
@@ -230,8 +230,10 @@ static TTF_SurfaceTextEngineData *CreateEngineData(void)
     return data;
 }
 
-static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text, TTF_DrawOperation *ops, int num_ops)
+static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_generation, TTF_Text *text)
 {
+    int num_ops = text->internal->num_ops;
+    const TTF_DrawOperation *ops = text->internal->ops;
     TTF_SurfaceTextEngineData *enginedata = (TTF_SurfaceTextEngineData *)userdata;
     TTF_SurfaceTextEngineFontData *fontdata;
     TTF_SurfaceTextEngineTextData *data;
@@ -250,13 +252,13 @@ static bool SDLCALL CreateText(void *userdata, TTF_Font *font, Uint32 font_gener
     if (!data) {
         return false;
     }
-    text->internal->textrep = data;
+    text->internal->engine_text = data;
     return true;
 }
 
 static void SDLCALL DestroyText(void *userdata, TTF_Text *text)
 {
-    TTF_SurfaceTextEngineTextData *data = (TTF_SurfaceTextEngineTextData *)text->internal->textrep;
+    TTF_SurfaceTextEngineTextData *data = (TTF_SurfaceTextEngineTextData *)text->internal->engine_text;
 
     (void)userdata;
     DestroyTextData(data);
@@ -331,7 +333,7 @@ bool TTF_DrawSurfaceText(TTF_Text *text, int x, int y, SDL_Surface *surface)
         return SDL_InvalidParamError("surface");
     }
 
-    data = (TTF_SurfaceTextEngineTextData *)text->internal->textrep;
+    data = (TTF_SurfaceTextEngineTextData *)text->internal->engine_text;
 
     if (text->color.r != data->fcolor.r ||
         text->color.g != data->fcolor.g ||
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 0cc00de6..b20bb2c2 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -216,6 +216,7 @@ typedef struct PosBuf {
     FT_UInt index;
     int x;
     int y;
+    int offset;
 } PosBuf_t;
 
 // The structure used to hold internal font information
@@ -1409,6 +1410,7 @@ static bool Render_Line_TextEngine(TTF_Font *font, int xstart, int ystart, int w
         FT_UInt idx = font->pos_buf[i].index;
         int x       = font->pos_buf[i].x;
         int y       = font->pos_buf[i].y;
+        int offset  = font->pos_buf[i].offset;
         c_glyph *glyph;
 
         if (Find_GlyphByIndex(font, idx, 0, 0, 0, 0, 0, &glyph, NULL)) {
@@ -1453,6 +1455,7 @@ static bool Render_Line_TextEngine(TTF_Font *font, int xstart, int ystart, int w
 
             op = &ops[op_index++];
             op->cmd = TTF_DRAW_COMMAND_COPY;
+            op->copy.text_offset = offset;
             op->copy.glyph_index = idx;
             op->copy.src.x = glyph_x;
             op->copy.src.y = glyph_y;
@@ -2856,6 +2859,24 @@ bool TTF_GetGlyphKerning(TTF_Font *font, Uint32 previous_ch, Uint32 ch, int *ker
     return true;
 }
 
+#if TTF_USE_HARFBUZZ
+static int CalculateUTF8Offset(const char *text, uint32_t cluster, size_t length)
+{
+    const char *end = text;
+    while (cluster > 0) {
+        Uint32 c = SDL_StepUTF8(&end, &length);
+        if (c == 0) {
+            break;
+        }
+        if (c == SDL_INVALID_UNICODE_CODEPOINT) {
+            continue;
+        }
+        --cluster;
+    }
+    return (int)(uintptr_t)(end - text);
+}
+#endif // TTF_USE_HARFBUZZ
+
 static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, int *w, int *h, int *xstart, int *ystart, int measure_width, int *extent, int *count)
 {
     int x = 0;
@@ -2945,6 +2966,8 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
     hb_glyph_position = hb_buffer_get_glyph_positions(hb_buffer, &glyph_count);
 
     // Load and render each character
+    uint32_t last_cluster = 0;
+    int offset = 0;
     for (g = 0; g < glyph_count; g++)
     {
         FT_UInt idx   = hb_glyph_info[g].codepoint;
@@ -2952,12 +2975,24 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
         int y_advance = hb_glyph_position[g].y_advance;
         int x_offset  = hb_glyph_position[g].x_offset;
         int y_offset  = hb_glyph_position[g].y_offset;
+
+        uint32_t cluster = hb_glyph_info[g].cluster;
+        if (cluster > last_cluster) {
+            offset += CalculateUTF8Offset(text + offset, cluster - last_cluster, length - offset);
+        } else if (cluster < last_cluster) {
+            offset = CalculateUTF8Offset(text, cluster, length);
+        }
+        last_cluster = cluster;
 #else
     // Load each character and sum it's bounding box
+    int offset = 0;
     while (length > 0) {
+        const char *last = text;
         Uint32 c = SDL_StepUTF8(&text, &length);
         FT_UInt idx = get_char_index(font, c);
 
+        offset += (text - last);
+
         if (c == UNICODE_BOM_NATIVE || c == UNICODE_BOM_SWAPPED) {
             continue;
         }
@@ -3022,9 +3057,10 @@ static bool TTF_Size_Internal(TTF_Font *font, const char *text, size_t length, i
         pos_y = F26Dot6(font->ascent);
 #endif
         // Store things for Render_Line()
-        font->pos_buf[font->pos_len].x     = pos_x;
-        font->pos_buf[font->pos_len].y     = pos_y;
-        font->pos_buf[font->pos_len].index = idx;
+        font->pos_buf[font->pos_len].x      = pos_x;
+        font->pos_buf[font->pos_len].y      = pos_y;
+        font->pos_buf[font->pos_len].index  = idx;
+        font->pos_buf[font->pos_len].offset = offset;
         font->pos_len += 1;
 
         // Compute provisional global bounding box
@@ -3553,23 +3589,32 @@ typedef struct TTF_InternalText
     TTF_TextData internal;
 } TTF_InternalText;
 
-static TTF_Text *CreateText(TTF_TextEngine *engine, int width, int height)
+static TTF_Text *CreateText(TTF_TextEngine *engine, const char *text, size_t length, int width, int height, TTF_DrawOperation *ops, int num_ops)
 {
     TTF_InternalText *mem = (TTF_InternalText *)SDL_calloc(1, sizeof(*mem));
     if (!mem) {
         return NULL;
     }
 
-    TTF_Text *text = &mem->text;
-    text->internal = &mem->internal;
-    text->w = width;
-    text->h = height;
-    text->color.r = 1.0f;
-    text->color.g = 1.0f;
-    text->color.b = 1.0f;
-    text->color.a = 1.0f;
-    text->internal->engine = engine;
-    return text;
+    TTF_Text *result = &mem->text;
+    result->internal = &mem->internal;
+    result->text = (char *)SDL_malloc(length + 1);
+    if (!result->text) {
+        SDL_free(mem);
+        return NULL;
+    }
+    SDL_memcpy(result->text, text, length);
+    result->text[length] = '\0';
+    result->w = width;
+    result->h = height;
+    result->color.r = 1.0f;
+    result->color.g = 1.0f;
+    result->color.b = 1.0f;
+    result->color.a = 1.0f;
+    result->internal->engine = engine;
+    result->internal->num_ops = num_ops;
+    result->internal->ops = ops;
+    return result;
 }
 
 TTF_Text *TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *text, size_t length)
@@ -3580,11 +3625,10 @@ TTF_Text *TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *tex
     TTF_Text *result = NULL;
 
     TTF_CHECK_INITIALIZED(NULL);
-    TTF_CHECK_POINTER("engine", engine, NULL);
     TTF_CHECK_POINTER("font", font, NULL);
     TTF_CHECK_POINTER("text", text, NULL);
 
-    if (engine->version < sizeof(*engine)) {
+    if (engine && engine->version < sizeof(*engine)) {
         // Update this to handle older versions of this interface
         SDL_SetError("Invalid engine, should be initialized with SDL_INIT_INTERFACE()");
         return 0;
@@ -3628,16 +3672,17 @@ TTF_Text *TTF_CreateText(TTF_TextEngine *engine, TTF_Font *font, const char *tex
         Draw_Line_TextEngine(font, width, height, 0, ystart + font->strikethrough_top_row, width, font->line_thickness, ops, &num_ops);
     }
 
-    result = CreateText(engine, width, height);
+    result = CreateText(engine, text, length, width, height, ops, num_ops);
     if (!result) {
         goto failure;
     }
 
-    if (!engine->CreateText(engine->userdata, font, font->generation, result, ops, num_ops)) {
-        goto failure;
+    if (engine) {
+        if (!engine->CreateText(engine->userdata, font, font->generation, result)) {
+            goto failure;
+        }
     }
 
-    SDL_free(ops);
     return result;
 
 failure:
@@ -3655,14 +3700,16 @@ TTF_Text *TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const c
     int num_ops = 0, max_ops = 0, extra_ops = 0, additional_ops;
     TTF_Text *result = NULL;
 
-    TTF_CHECK_POINTER("engine", engine, NULL);
-
-    if (engine->version < sizeof(*engine)) {
+    if (engine && engine->version < sizeof(*engine)) {
         // Update this to handle older versions of this interface
         SDL_SetError("Invalid engine, should be initialized with SDL_INIT_INTERFACE()");
         return 0;
     }
 
+    if (!length) {
+        length = SDL_strlen(text);
+    }
+
     if (!GetWrappedLines(font, text, length, wrapLength, &strLines, &numLines, &width, &height)) {
         return NULL;
     }
@@ -3721,17 +3768,18 @@ TTF_Text *TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const c
         }
     }
 
-    result = CreateText(engine, width, height);
+    result = CreateText(engine, text, length, width, height, ops, num_ops);
     if (!result) {
         goto failure;
     }
 
-    if (!engine->CreateText(engine->userdata, font, font->generation, result, ops, num_ops)) {
-        goto failure;
+    if (engine) {
+        if (!engine->CreateText(engine->userdata, font, font->generation, result)) {
+            goto failure;
+        }
     }
 
     SDL_free(strLines);
-    SDL_free(ops);
     return result;
 
 failure:
@@ -3744,7 +3792,6 @@ TTF_Text *TTF_CreateText_Wrapped(TTF_TextEngine *engine, TTF_Font *font, const c
 SDL_PropertiesID TTF_GetTextProperties(TTF_Text *text)
 {
     TTF_CHECK_POINTER("text", text, 0);
-    TTF_CHECK_POINTER("text", text->internal, 0);
 
     if (!text->internal->props) {
         text->internal->props = SDL_CreateProperties();
@@ -3754,15 +3801,17 @@ SDL_PropertiesID TTF_GetTextProperties(TTF_Text *text)
 
 void TTF_DestroyText(TTF_Text *text)
 {
-    if (!text || !text->internal) {
+    if (!text) {
         return;
     }
 
     TTF_TextEngine *engine = text->internal->engine;
-    engine->DestroyText(engine->userdata, text);
+    if (engine) {
+        engine->DestroyText(engine->userdata, text);
+    }
     SDL_DestroyProperties(text->internal->props);
-    text->internal = NULL;
-    SDL_free(text->label);
+    SDL_free(text->internal->ops);
+    SDL_free(text->text);
     SDL_free(text);
 }