SDL_ttf: Added a separate end-of-text cluster

From aebd2d2545c40e43dbe5aa03c1c2e97ae9677cf7 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 7 Oct 2024 00:28:15 -0700
Subject: [PATCH] Added a separate end-of-text cluster

This allows moving the cursor past a newline at the end of the text.
---
 examples/editbox.c |  2 +-
 src/SDL_ttf.c      | 68 +++++++++++++++++++++++++++-------------------
 2 files changed, 41 insertions(+), 29 deletions(-)

diff --git a/examples/editbox.c b/examples/editbox.c
index d9e807f7..f96d5087 100644
--- a/examples/editbox.c
+++ b/examples/editbox.c
@@ -456,7 +456,7 @@ void EditBox_Draw(EditBox *edit)
 
 static int GetCursorTextIndex(TTF_Font *font, int x, const TTF_SubString *substring)
 {
-    if (substring->flags & TTF_SUBSTRING_LINE_END) {
+    if (substring->flags & (TTF_SUBSTRING_LINE_END | TTF_SUBSTRING_TEXT_END)) {
         return substring->offset;
     }
 
diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c
index 3fc57a2d..59a45d05 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -3724,10 +3724,11 @@ static int SDLCALL SortClusters(const void *a, const void *b)
     return (A->offset - B->offset);
 }
 
-static int CalculateClusterLengths(TTF_Font *font, TTF_SubString *clusters, int num_clusters, size_t length, int *lines)
+static int CalculateClusterLengths(TTF_Text *text, TTF_SubString *clusters, int num_clusters, size_t length, int *lines)
 {
     SDL_qsort(clusters, num_clusters, sizeof(*clusters), SortClusters);
 
+    TTF_Font *font = text->internal->font;
     int i;
     const TTF_SubString *src = clusters;
     TTF_SubString *last = NULL;
@@ -3769,13 +3770,28 @@ static int CalculateClusterLengths(TTF_Font *font, TTF_SubString *clusters, int
                 cluster->rect.x += cluster->rect.w;
                 cluster->rect.w = 0;
             }
+        } else if (cluster->flags & TTF_SUBSTRING_TEXT_END) {
+            if (last) {
+                if (last->length > 0 && text->text[last->offset + last->length - 1] == '\n') {
+                    cluster->line_index = last->line_index + 1;
+                    cluster->rect.y = (cluster->line_index * font->lineskip);
+                    cluster->rect.h = font->height;
+                } else {
+                    cluster->line_index = last->line_index;
+                    SDL_copyp(&cluster->rect, &last->rect);
+                    cluster->rect.x += cluster->rect.w;
+                    cluster->rect.w = 0;
+                }
+            } else {
+                cluster->rect.h = font->height;
+            }
         }
 
         if (i < (num_clusters - 1)) {
             cluster->length = clusters[i + 1].offset - cluster->offset;
         } else {
-            cluster->flags |= TTF_SUBSTRING_TEXT_END;
-            cluster->length = (int)(length - cluster->offset);
+            SDL_assert(cluster->flags & TTF_SUBSTRING_TEXT_END);
+            SDL_assert(cluster->offset == length);
         }
         last = cluster;
     }
@@ -3810,7 +3826,7 @@ static bool LayoutText(TTF_Text *text)
         goto done;
     }
 
-    clusters = (TTF_SubString *)SDL_calloc(font->num_clusters + 1, sizeof(*clusters));
+    clusters = (TTF_SubString *)SDL_calloc(font->num_clusters + 2, sizeof(*clusters));
     if (!clusters) {
         goto done;
     }
@@ -3819,10 +3835,15 @@ static bool LayoutText(TTF_Text *text)
     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);
@@ -3832,7 +3853,7 @@ static bool LayoutText(TTF_Text *text)
         Draw_Line_TextEngine(font, width, height, 0, ystart + font->strikethrough_top_row, width, font->line_thickness, ops, &num_ops);
     }
 
-    num_clusters = CalculateClusterLengths(font, clusters, num_clusters, length, NULL);
+    num_clusters = CalculateClusterLengths(text, clusters, num_clusters, length, NULL);
 
     result = true;
 
@@ -3862,7 +3883,7 @@ static bool LayoutTextWrapped(TTF_Text *text)
     TTF_DrawOperation *ops = NULL, *new_ops;
     int num_ops = 0, max_ops = 0, extra_ops = 0, additional_ops;
     TTF_SubString *clusters = NULL, *new_clusters, *cluster;
-    int num_clusters = 0, max_clusters = 0, additional_clusters, cluster_offset;
+    int num_clusters = 0, max_clusters = 0, cluster_offset;
     int *lines = NULL;
     bool result = false;
 
@@ -3887,20 +3908,18 @@ static bool LayoutTextWrapped(TTF_Text *text)
         }
     }
 
+    max_clusters = numLines + 1;
+    clusters = (TTF_SubString *)SDL_calloc(max_clusters, sizeof(*clusters));
+    if (!clusters) {
+        goto done;
+    }
+
     // Render each line
     for (i = 0; i < numLines; i++) {
         int xstart, ystart, line_width, xoffset;
 
         if (strLines[i].length == 0) {
-            new_clusters = (TTF_SubString *)SDL_realloc(clusters, (max_clusters + 1) * sizeof(*new_clusters));
-            if (!new_clusters) {
-                goto done;
-            }
-            clusters = new_clusters;
-            max_clusters += 1;
-
             cluster = &clusters[num_clusters++];
-            SDL_zerop(cluster);
             cluster->flags = TTF_SUBSTRING_LINE_END;
             cluster->offset = (int)(uintptr_t)(strLines[i].text - text->text);
             cluster->line_index = i;
@@ -3936,14 +3955,13 @@ static bool LayoutTextWrapped(TTF_Text *text)
         max_ops += additional_ops;
 
         // Allocate space for the clusters on this line
-        additional_clusters = (font->num_clusters + 1);
-        new_clusters = (TTF_SubString *)SDL_realloc(clusters, (max_clusters + additional_clusters) * sizeof(*new_clusters));
+        new_clusters = (TTF_SubString *)SDL_realloc(clusters, (max_clusters + font->num_clusters) * sizeof(*new_clusters));
         if (!new_clusters) {
             goto done;
         }
-        SDL_memset(new_clusters + max_clusters, 0, additional_clusters * sizeof(*new_clusters));
+        SDL_memset(new_clusters + max_clusters, 0, font->num_clusters * sizeof(*new_clusters));
         clusters = new_clusters;
-        max_clusters += additional_clusters;
+        max_clusters += font->num_clusters;
         cluster_offset = (int)(uintptr_t)(strLines[i].text - text->text);
 
         // Create the text drawing operations
@@ -3964,8 +3982,11 @@ static bool LayoutTextWrapped(TTF_Text *text)
             Draw_Line_TextEngine(font, width, height, xoffset, ystart + font->strikethrough_top_row, line_width, font->line_thickness, ops, &num_ops);
         }
     }
+    cluster = &clusters[num_clusters++];
+    cluster->flags = TTF_SUBSTRING_TEXT_END;
+    cluster->offset = (int)length;
 
-    num_clusters = CalculateClusterLengths(font, clusters, num_clusters, length, lines);
+    num_clusters = CalculateClusterLengths(text, clusters, num_clusters, length, lines);
 
     result = true;
 
@@ -4293,9 +4314,6 @@ bool TTF_GetTextSubString(TTF_Text *text, int offset, TTF_SubString *substring)
     int length = (int)SDL_strlen(text->text);
     if (offset >= length) {
         SDL_copyp(substring, &clusters[num_clusters - 1]);
-        substring->length = 0;
-        substring->rect.x += substring->rect.w;
-        substring->rect.w = 0;
         return true;
     }
 
@@ -4366,9 +4384,6 @@ bool TTF_GetTextSubStringForLine(TTF_Text *text, int line, TTF_SubString *substr
 
     if (line >= text->num_lines) {
         SDL_copyp(substring, &clusters[num_clusters - 1]);
-        substring->length = 0;
-        substring->rect.x += substring->rect.w;
-        substring->rect.w = 0;
         return true;
     }
 
@@ -4630,9 +4645,6 @@ bool TTF_GetNextTextSubString(TTF_Text *text, const TTF_SubString *substring, TT
     }
     if (substring->cluster_index == (num_clusters - 1)) {
         SDL_copyp(next, &clusters[num_clusters - 1]);
-        next->length = 0;
-        next->rect.x += next->rect.w;
-        next->rect.w = 0;
     } else {
         SDL_copyp(next, &clusters[substring->cluster_index + 1]);
     }