SDL_ttf: Added simple text input to showfont

From 426cd5ed1fb5f2192f4c4d8849cfa58c209eefa8 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 3 Oct 2024 08:38:40 -0700
Subject: [PATCH] Added simple text input to showfont

---
 examples/showfont.c           | 137 ++++++++++++++++++++++++++++++----
 src/SDL_renderer_textengine.c |  12 +--
 src/SDL_surface_textengine.c  |   8 +-
 src/SDL_ttf.c                 |  13 +++-
 4 files changed, 144 insertions(+), 26 deletions(-)

diff --git a/examples/showfont.c b/examples/showfont.c
index 24ad8fe3..6c6339b8 100644
--- a/examples/showfont.c
+++ b/examples/showfont.c
@@ -237,12 +237,27 @@ static void MoveCursorDown(Scene *scene)
     }
 }
 
+static void SetTextFocus(Scene *scene, bool focused)
+{
+    if (!scene->text) {
+        return;
+    }
+
+    scene->textFocus = focused;
+
+    if (focused) {
+        SDL_StartTextInput(scene->window);
+    } else {
+        SDL_StopTextInput(scene->window);
+    }
+}
+
 static void HandleTextClick(Scene *scene, float x, float y)
 {
     TTF_SubString substring;
 
     if (!scene->textFocus) {
-        scene->textFocus = true;
+        SetTextFocus(scene, true);
         return;
     }
 
@@ -574,7 +589,7 @@ int main(int argc, char *argv[])
                         if (SDL_PointInRectFloat(&pt, &scene.textRect)) {
                             HandleTextClick(&scene, pt.x, pt.y);
                         } else if (scene.textFocus) {
-                            scene.textFocus = false;
+                            SetTextFocus(&scene, false);
                         } else {
                             scene.messageRect.x = (event.button.x - text->w/2);
                             scene.messageRect.y = (event.button.y - text->h/2);
@@ -618,9 +633,9 @@ int main(int argc, char *argv[])
                         }
                         break;
                     case SDLK_C:
-                        /* Copy to clipboard */
-                        if (event.key.mod & SDL_KMOD_CTRL) {
-                            if (scene.text) {
+                        if (scene.textFocus) {
+                            /* Copy to clipboard */
+                            if (event.key.mod & SDL_KMOD_CTRL) {
                                 SDL_SetClipboardText(scene.text->text);
                             }
                         }
@@ -688,26 +703,54 @@ int main(int argc, char *argv[])
                         }
                         break;
                     case SDLK_V:
-                        /* Paste from clipboard */
-                        if (event.key.mod & SDL_KMOD_CTRL) {
-                            if (scene.text) {
-                                TTF_SetTextString(scene.text, SDL_GetClipboardText(), 0);
+                        if (scene.textFocus) {
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Paste from clipboard */
+                                const char *text = SDL_GetClipboardText();
+                                size_t length = SDL_strlen(text);
+                                TTF_InsertTextString(scene.text, scene.cursor, text, length);
+                                scene.cursor = (int)(scene.cursor + length);
+                            }
+                        }
+                        break;
+                    case SDLK_X:
+                        if (scene.textFocus) {
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Copy to clipboard and delete text */
+                                if (scene.text->text) {
+                                    SDL_SetClipboardText(scene.text->text);
+                                    TTF_DeleteTextString(scene.text, 0, -1);
+                                }
                             }
                         }
                         break;
                     case SDLK_LEFT:
                         if (scene.textFocus) {
-                            MoveCursorLeft(&scene);
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Move to the beginning of the line (FIXME) */
+                                scene.cursor = 0;
+                            } else {
+                                MoveCursorLeft(&scene);
+                            }
                         }
                         break;
                     case SDLK_RIGHT:
                         if (scene.textFocus) {
-                            MoveCursorRight(&scene);
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Move to the end of the line (FIXME) */
+                            } else {
+                                MoveCursorRight(&scene);
+                            }
                         }
                         break;
                     case SDLK_UP:
                         if (scene.textFocus) {
-                            MoveCursorUp(&scene);
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Move to the beginning of the text */
+                                scene.cursor = 0;
+                            } else {
+                                MoveCursorUp(&scene);
+                            }
                         } else {
                             /* Increase font size */
                             ptsize = TTF_GetFontSize(font);
@@ -716,16 +759,75 @@ int main(int argc, char *argv[])
                         break;
                     case SDLK_DOWN:
                         if (scene.textFocus) {
-                            MoveCursorDown(&scene);
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Move to the end of the text */
+                                if (scene.text->text) {
+                                    scene.cursor = (int)SDL_strlen(scene.text->text);
+                                }
+                            } else {
+                                MoveCursorDown(&scene);
+                            }
                         } else {
                             /* Decrease font size */
                             ptsize = TTF_GetFontSize(font);
                             TTF_SetFontSize(font, ptsize - 1.0f);
                         }
                         break;
+                    case SDLK_HOME:
+                        if (scene.textFocus) {
+                            /* Move to the beginning of the text */
+                            scene.cursor = 0;
+                        }
+                        break;
+                    case SDLK_END:
+                        if (scene.textFocus) {
+                            /* Move to the end of the text */
+                            if (scene.text->text) {
+                                scene.cursor = (int)SDL_strlen(scene.text->text);
+                            }
+                        }
+                        break;
+                    case SDLK_BACKSPACE:
+                        if (scene.textFocus) {
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Delete to the beginning of the string */
+                                TTF_DeleteTextString(scene.text, 0, scene.cursor);
+                                scene.cursor = 0;
+                            } else if (scene.text->text) {
+                                const char *start = &scene.text->text[scene.cursor];
+                                const char *current = start;
+                                /* Step back over the previous UTF-8 character */
+                                do {
+                                    if (current == scene.text->text) {
+                                        break;
+                                    }
+                                    --current;
+                                } while ((*current & 0xC0) == 0x80);
+
+                                int length = (int)(start - current);
+                                TTF_DeleteTextString(scene.text, scene.cursor - length, length);
+                                scene.cursor -= length;
+                            }
+                        }
+                        break;
+                    case SDLK_DELETE:
+                        if (scene.textFocus) {
+                            if (event.key.mod & SDL_KMOD_CTRL) {
+                                /* Delete to the end of the string */
+                                TTF_DeleteTextString(scene.text, scene.cursor, -1);
+                            } else if (scene.text->text) {
+                                const char *start = &scene.text->text[scene.cursor];
+                                const char *next = start;
+                                size_t length = SDL_strlen(next);
+                                SDL_StepUTF8(&next, &length);
+                                length = (next - start);
+                                TTF_DeleteTextString(scene.text, scene.cursor, (int)length);
+                            }
+                        }
+                        break;
                     case SDLK_ESCAPE:
                         if (scene.textFocus) {
-                            scene.textFocus = false;
+                            SetTextFocus(&scene, false);
                         } else {
                             done = true;
                         }
@@ -735,6 +837,13 @@ int main(int argc, char *argv[])
                     }
                     break;
 
+                case SDL_EVENT_TEXT_INPUT:
+                    if (scene.text) {
+                        size_t length = SDL_strlen(event.text.text);
+                        TTF_InsertTextString(scene.text, scene.cursor, event.text.text, length);
+                        scene.cursor = (int)(scene.cursor + length);
+                    }
+                    break;
                 case SDL_EVENT_QUIT:
                     done = true;
                     break;
diff --git a/src/SDL_renderer_textengine.c b/src/SDL_renderer_textengine.c
index fbe08408..a7140461 100644
--- a/src/SDL_renderer_textengine.c
+++ b/src/SDL_renderer_textengine.c
@@ -850,9 +850,6 @@ TTF_TextEngine *TTF_CreateRendererTextEngine(SDL_Renderer *renderer)
 
 bool TTF_DrawRendererText(TTF_Text *text, float x, float y)
 {
-    TTF_RendererTextEngineTextData *data;
-    SDL_Renderer *renderer;
-
     if (!text || !text->internal || text->internal->engine->CreateText != CreateText) {
         return SDL_InvalidParamError("text");
     }
@@ -862,8 +859,13 @@ bool TTF_DrawRendererText(TTF_Text *text, float x, float y)
         return false;
     }
 
-    renderer = ((TTF_RendererTextEngineData *)text->internal->engine->userdata)->renderer;
-    data = (TTF_RendererTextEngineTextData *)text->internal->engine_text;
+    TTF_RendererTextEngineTextData *data = (TTF_RendererTextEngineTextData *)text->internal->engine_text;
+    if (!data) {
+        // Empty string, nothing to do
+        return true;
+    }
+
+    SDL_Renderer *renderer = ((TTF_RendererTextEngineData *)text->internal->engine->userdata)->renderer;
     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 2860a514..f8a70754 100644
--- a/src/SDL_surface_textengine.c
+++ b/src/SDL_surface_textengine.c
@@ -326,8 +326,6 @@ static void DrawCopy(TTF_SurfaceTextEngineTextData *data, const TTF_CopyOperatio
 
 bool TTF_DrawSurfaceText(TTF_Text *text, int x, int y, SDL_Surface *surface)
 {
-    TTF_SurfaceTextEngineTextData *data;
-
     if (!text || !text->internal || text->internal->engine->CreateText != CreateText) {
         return SDL_InvalidParamError("text");
     }
@@ -340,7 +338,11 @@ bool TTF_DrawSurfaceText(TTF_Text *text, int x, int y, SDL_Surface *surface)
         return false;
     }
 
-    data = (TTF_SurfaceTextEngineTextData *)text->internal->engine_text;
+    TTF_SurfaceTextEngineTextData *data = (TTF_SurfaceTextEngineTextData *)text->internal->engine_text;
+    if (!data) {
+        // Empty string, nothing to do
+        return true;
+    }
 
     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 54294995..99ffe11d 100644
--- a/src/SDL_ttf.c
+++ b/src/SDL_ttf.c
@@ -4019,17 +4019,21 @@ bool TTF_InsertTextString(TTF_Text *text, int offset, const char *string, size_t
 {
     TTF_CHECK_POINTER("text", text, false);
 
-    if (!string || !*string || !length) {
+    if (!string || !*string) {
         return true;
     }
 
+    if (!length) {
+        length = SDL_strlen(string);
+    }
+
     if (!text->text) {
         return TTF_SetTextString(text, string, length);
     }
 
     int old_length = (int)SDL_strlen(text->text);
-    size_t new_length = old_length + length + 1;
-    char *new_string = (char *)SDL_realloc(text->text, new_length);
+    size_t new_length = old_length + length;
+    char *new_string = (char *)SDL_realloc(text->text, new_length + 1);
     if (!new_string) {
         return false;
     }
@@ -4047,7 +4051,7 @@ bool TTF_InsertTextString(TTF_Text *text, int offset, const char *string, size_t
 
     int shift = (old_length - offset);
     if (shift > 0) {
-        SDL_memcpy(new_string + offset + shift, new_string + offset, shift);
+        SDL_memmove(new_string + offset + length, new_string + offset, shift);
     }
     SDL_memcpy(new_string + offset, string, length);
     new_string[new_length] = '\0';
@@ -4177,6 +4181,7 @@ bool SDLCALL TTF_GetTextSubString(TTF_Text *text, int offset, TTF_SubString *sub
     }
 
     if (text->internal->num_clusters == 0) {
+        substring->rect.h = text->internal->font->height;
         return true;
     }