SDL_ttf: Refactored editbox functionality out into a separate file

From 4be7c39b59c66d1e7f486a2b45a888ad39eb06a0 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 3 Oct 2024 21:57:54 -0700
Subject: [PATCH] Refactored editbox functionality out into a separate file

This allows it to be easier to understand and reuse in other projects.
---
 CMakeLists.txt                    |   2 +-
 VisualC/showfont/showfont.vcxproj |  10 +-
 examples/editbox.c                | 599 +++++++++++++++++++++++++++++
 examples/editbox.h                |  53 +++
 examples/showfont.c               | 620 ++++++------------------------
 5 files changed, 769 insertions(+), 515 deletions(-)
 create mode 100644 examples/editbox.c
 create mode 100644 examples/editbox.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index edaf13c4..46a0ca99 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -356,7 +356,7 @@ endif()
 
 if(SDLTTF_SAMPLES)
     add_executable(glfont examples/glfont.c)
-    add_executable(showfont examples/showfont.c)
+    add_executable(showfont examples/showfont.c examples/editbox.c)
     add_executable(testapp examples/testapp.c)
 
     set(OpenGL_GL_PREFERENCE GLVND)
diff --git a/VisualC/showfont/showfont.vcxproj b/VisualC/showfont/showfont.vcxproj
index fbb7e186..bad7b39e 100644
--- a/VisualC/showfont/showfont.vcxproj
+++ b/VisualC/showfont/showfont.vcxproj
@@ -234,14 +234,10 @@
     </ProjectReference>
   </ItemGroup>
   <ItemGroup>
-    <ClCompile Include="..\..\examples\showfont.c">
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-      <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|x64'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
-    </ClCompile>
+    <ClCompile Include="..\..\examples\editbox.c"/>
+    <ClCompile Include="..\..\examples\showfont.c"/>
   </ItemGroup>
   <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
   <ImportGroup Label="ExtensionTargets">
   </ImportGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/examples/editbox.c b/examples/editbox.c
new file mode 100644
index 00000000..22fbef48
--- /dev/null
+++ b/examples/editbox.c
@@ -0,0 +1,599 @@
+/*
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+#include "editbox.h"
+
+#define CURSOR_BLINK_INTERVAL_MS    500
+
+
+static bool GetHighlightExtents(EditBox *edit, int *marker1, int *marker2)
+{
+    if (edit->highlight1 >= 0 && edit->highlight2 >= 0) {
+        *marker1 = SDL_min(edit->highlight1, edit->highlight2);
+        *marker2 = SDL_max(edit->highlight1, edit->highlight2) - 1;
+        if (*marker2 > *marker1) {
+            return true;
+        }
+    }
+    return false;
+}
+
+EditBox *EditBox_Create(TTF_Text *text, const SDL_FRect *rect)
+{
+    EditBox *edit = (EditBox *)SDL_calloc(1, sizeof(*edit));
+    if (!edit) {
+        return NULL;
+    }
+
+    edit->text = text;
+    edit->font = TTF_GetTextFont(text);
+    edit->rect = *rect;
+    edit->highlight1 = -1;
+    edit->highlight2 = -1;
+
+    return edit;
+}
+
+void EditBox_Destroy(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    SDL_free(edit);
+}
+
+void EditBox_Draw(EditBox *edit, SDL_Renderer *renderer)
+{
+    if (!edit) {
+        return;
+    }
+
+    float x = edit->rect.x;
+    float y = edit->rect.y;
+
+    /* Draw any highlight */
+    int marker1, marker2;
+    if (GetHighlightExtents(edit, &marker1, &marker2)) {
+        TTF_SubString **highlights = TTF_GetTextSubStringsForRange(edit->text, marker1, marker2, NULL);
+        if (highlights) {
+            int i;
+            SDL_SetRenderDrawColor(renderer, 0xCC, 0xCC, 0x00, 0xFF);
+            for (i = 0; highlights[i]; ++i) {
+                SDL_FRect rect;
+                SDL_RectToFRect(&highlights[i]->rect, &rect);
+                rect.x += x;
+                rect.y += y;
+                SDL_RenderFillRect(renderer, &rect);
+            }
+            SDL_free(highlights);
+        }
+    }
+
+    if (edit->window_surface) {
+        /* Flush the renderer so we can draw directly to the window surface */
+        SDL_FlushRenderer(renderer);
+        TTF_DrawSurfaceText(edit->text, (int)x, (int)y, edit->window_surface);
+    } else {
+        TTF_DrawRendererText(edit->text, x, y);
+    }
+
+    /* Draw the cursor */
+    Uint64 now = SDL_GetTicks();
+    if ((now - edit->last_cursor_change) >= CURSOR_BLINK_INTERVAL_MS) {
+        edit->cursor_visible = !edit->cursor_visible;
+        edit->last_cursor_change = now;
+    }
+
+    TTF_SubString cursor;
+    if (edit->cursor_visible && TTF_GetTextSubString(edit->text, edit->cursor, &cursor)) {
+        SDL_FRect cursorRect;
+        if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+            cursorRect.x = x + cursor.rect.x + cursor.rect.w;
+        } else {
+            cursorRect.x = x + cursor.rect.x;
+        }
+        cursorRect.y = y + cursor.rect.y;
+        cursorRect.w = 1.0f;
+        cursorRect.h = (float)cursor.rect.h;
+
+        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF);
+        SDL_RenderFillRect(renderer, &cursorRect);
+    }
+}
+
+static int GetCursorTextIndex(TTF_Font *font, int x, const TTF_SubString *substring)
+{
+    bool round_down;
+    if (TTF_GetFontDirection(font) == TTF_DIRECTION_RTL) {
+        round_down = (x > (substring->rect.x + substring->rect.w / 2));
+    } else {
+        round_down = (x < (substring->rect.x + substring->rect.w / 2));
+    }
+    if (round_down) {
+        /* Start the cursor before the selected text */
+        return substring->offset;
+    } else {
+        /* Place the cursor after the selected text */
+        return substring->offset + substring->length;
+    }
+}
+
+static void MoveCursorIndex(EditBox *edit, int direction)
+{
+    TTF_SubString substring;
+
+    if (direction < 0) {
+        if (TTF_GetTextSubString(edit->text, edit->cursor - 1, &substring)) {
+            edit->cursor = substring.offset;
+        }
+    } else {
+        if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
+            TTF_GetTextSubString(edit->text, substring.offset + substring.length, &substring)) {
+            edit->cursor = substring.offset;
+        }
+    }
+}
+
+void EditBox_MoveCursorLeft(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+        MoveCursorIndex(edit, 1);
+    } else {
+        MoveCursorIndex(edit, -1);
+    }
+}
+
+void EditBox_MoveCursorRight(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+        MoveCursorIndex(edit, -1);
+    } else {
+        MoveCursorIndex(edit, 1);
+    }
+}
+
+void EditBox_MoveCursorUp(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    TTF_SubString substring;
+    if (TTF_GetTextSubString(edit->text, edit->cursor, &substring)) {
+        int fontHeight = TTF_GetFontHeight(edit->font);
+        int x, y;
+        if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+            x = substring.rect.x + substring.rect.w;
+        } else {
+            x = substring.rect.x;
+        }
+        y = substring.rect.y - fontHeight;
+        if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
+            edit->cursor = GetCursorTextIndex(edit->font, x, &substring);
+        }
+    }
+}
+
+void EditBox_MoveCursorDown(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    TTF_SubString substring;
+    if (TTF_GetTextSubString(edit->text, edit->cursor, &substring)) {
+        int fontHeight = TTF_GetFontHeight(edit->font);
+        int x, y;
+        if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+            x = substring.rect.x + substring.rect.w;
+        } else {
+            x = substring.rect.x;
+        }
+        y = substring.rect.y + substring.rect.h + fontHeight;
+        if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
+            edit->cursor = GetCursorTextIndex(edit->font, x, &substring);
+        }
+    }
+}
+
+void EditBox_MoveCursorBeginningOfLine(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    TTF_SubString substring;
+    if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
+        TTF_GetTextSubStringForLine(edit->text, substring.line_index, &substring)) {
+        edit->cursor = substring.offset;
+    }
+}
+
+void EditBox_MoveCursorEndOfLine(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    TTF_SubString substring;
+    if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
+        TTF_GetTextSubStringForLine(edit->text, substring.line_index, &substring)) {
+        edit->cursor = substring.offset + substring.length;
+    }
+}
+
+void EditBox_MoveCursorBeginning(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    /* Move to the beginning of the text */
+    edit->cursor = 0;
+}
+
+void EditBox_MoveCursorEnd(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    /* Move to the end of the text */
+    if (edit->text->text) {
+        edit->cursor = (int)SDL_strlen(edit->text->text);
+    }
+}
+
+void EditBox_Backspace(EditBox *edit)
+{
+    if (!edit || !edit->text->text) {
+        return;
+    }
+
+    if (EditBox_DeleteHighlight(edit)) {
+        return;
+    }
+
+    const char *start = &edit->text->text[edit->cursor];
+    const char *current = start;
+    /* Step back over the previous UTF-8 character */
+    do {
+        if (current == edit->text->text) {
+            break;
+        }
+        --current;
+    } while ((*current & 0xC0) == 0x80);
+
+    int length = (int)(start - current);
+    TTF_DeleteTextString(edit->text, edit->cursor - length, length);
+    edit->cursor -= length;
+}
+
+void EditBox_BackspaceToBeginning(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    /* Delete to the beginning of the string */
+    TTF_DeleteTextString(edit->text, 0, edit->cursor);
+    edit->cursor = 0;
+}
+
+void EditBox_DeleteToEnd(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    /* Delete to the end of the string */
+    TTF_DeleteTextString(edit->text, edit->cursor, -1);
+}
+
+void EditBox_Delete(EditBox *edit)
+{
+    if (!edit || !edit->text->text) {
+        return;
+    }
+
+    if (EditBox_DeleteHighlight(edit)) {
+        return;
+    }
+
+    const char *start = &edit->text->text[edit->cursor];
+    const char *next = start;
+    size_t length = SDL_strlen(next);
+    SDL_StepUTF8(&next, &length);
+    length = (next - start);
+    TTF_DeleteTextString(edit->text, edit->cursor, (int)length);
+}
+
+static bool HandleMouseDown(EditBox *edit, float x, float y)
+{
+    SDL_FPoint pt = { x, y };
+    if (!SDL_PointInRectFloat(&pt, &edit->rect)) {
+        return false;
+    }
+
+    /* Set the cursor position */
+    TTF_SubString substring;
+    int textX = (int)SDL_roundf(x - (edit->rect.x + 4.0f));
+    int textY = (int)SDL_roundf(y - (edit->rect.y + 4.0f));
+    if (!TTF_GetTextSubStringForPoint(edit->text, textX, textY, &substring)) {
+        SDL_Log("Couldn't get cursor location: %s\n", SDL_GetError());
+        return false;
+    }
+
+    edit->cursor = GetCursorTextIndex(edit->font, textX, &substring);
+    edit->highlighting = true;
+    edit->highlight1 = edit->cursor;
+    edit->highlight2 = -1;
+
+    return true;
+}
+
+static bool HandleMouseMotion(EditBox *edit, float x, float y)
+{
+    if (!edit->highlighting) {
+        return false;
+    }
+
+    /* Set the highlight position */
+    TTF_SubString substring;
+    int textX = (int)SDL_roundf(x - (edit->rect.x + 4.0f));
+    int textY = (int)SDL_roundf(y - (edit->rect.y + 4.0f));
+    if (!TTF_GetTextSubStringForPoint(edit->text, textX, textY, &substring)) {
+        SDL_Log("Couldn't get cursor location: %s\n", SDL_GetError());
+        return false;
+    }
+
+    edit->cursor = GetCursorTextIndex(edit->font, textX, &substring);
+    edit->highlight2 = edit->cursor;
+
+    return true;
+}
+
+static bool HandleMouseUp(EditBox *edit, float x, float y)
+{
+    (void)x; (void)y;
+
+    if (!edit->highlighting) {
+        return false;
+    }
+
+    edit->highlighting = false;
+    return true;
+}
+
+void EditBox_SelectAll(EditBox *edit)
+{
+    if (!edit || !edit->text->text) {
+        return;
+    }
+
+    edit->highlight1 = 0;
+    edit->highlight2 = (int)SDL_strlen(edit->text->text);
+}
+
+bool EditBox_DeleteHighlight(EditBox *edit)
+{
+    if (!edit || !edit->text->text) {
+        return false;
+    }
+
+    int marker1, marker2;
+    if (GetHighlightExtents(edit, &marker1, &marker2)) {
+        size_t length = marker2 - marker1 + 1;
+        TTF_DeleteTextString(edit->text, marker1, (int)length);
+        edit->cursor = marker1;
+        edit->highlight1 = -1;
+        edit->highlight2 = -1;
+        return true;
+    }
+    return false;
+}
+
+void EditBox_Copy(EditBox *edit)
+{
+    if (!edit || !edit->text->text) {
+        return;
+    }
+
+    int marker1, marker2;
+    if (GetHighlightExtents(edit, &marker1, &marker2)) {
+        size_t length = marker2 - marker1 + 1;
+        char *temp = (char *)SDL_malloc(length + 1);
+        if (temp) {
+            SDL_memcpy(temp, &edit->text->text[marker1], length);
+            temp[length] = '\0';
+            SDL_SetClipboardText(temp);
+            SDL_free(temp);
+        }
+    } else {
+        SDL_SetClipboardText(edit->text->text);
+    }
+}
+
+void EditBox_Cut(EditBox *edit)
+{
+    if (!edit || !edit->text->text) {
+        return;
+    }
+
+    /* Copy to clipboard and delete text */
+    int marker1, marker2;
+    if (GetHighlightExtents(edit, &marker1, &marker2)) {
+        size_t length = marker2 - marker1 + 1;
+        char *temp = (char *)SDL_malloc(length + 1);
+        if (temp) {
+            SDL_memcpy(temp, &edit->text->text[marker1], length);
+            temp[length] = '\0';
+            SDL_SetClipboardText(edit->text->text);
+            SDL_free(temp);
+        }
+        TTF_DeleteTextString(edit->text, marker1, (int)length);
+        edit->cursor = marker1;
+        edit->highlight1 = -1;
+        edit->highlight2 = -1;
+    } else {
+        SDL_SetClipboardText(edit->text->text);
+        TTF_DeleteTextString(edit->text, 0, -1);
+    }
+}
+
+void EditBox_Paste(EditBox *edit)
+{
+    if (!edit) {
+        return;
+    }
+
+    const char *text = SDL_GetClipboardText();
+    size_t length = SDL_strlen(text);
+    TTF_InsertTextString(edit->text, edit->cursor, text, length);
+    edit->cursor = (int)(edit->cursor + length);
+}
+
+void EditBox_Insert(EditBox *edit, const char *text)
+{
+    if (!edit || !text) {
+        return;
+    }
+
+    size_t length = SDL_strlen(text);
+    TTF_InsertTextString(edit->text, edit->cursor, text, length);
+    edit->cursor = (int)(edit->cursor + length);
+}
+
+bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
+{
+    if (!edit || !event) {
+        return false;
+    }
+
+    switch (event->type) {
+    case SDL_EVENT_MOUSE_BUTTON_DOWN:
+        return HandleMouseDown(edit, event->button.x, event->button.y);
+
+    case SDL_EVENT_MOUSE_MOTION:
+        return HandleMouseMotion(edit, event->motion.x, event->motion.y);
+
+    case SDL_EVENT_MOUSE_BUTTON_UP:
+        return HandleMouseUp(edit, event->button.x, event->button.y);
+
+    case SDL_EVENT_KEY_DOWN:
+        switch (event->key.key) {
+        case SDLK_A:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_SelectAll(edit);
+                return true;
+            }
+            break;
+        case SDLK_C:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_Copy(edit);
+                return true;
+            }
+            break;
+
+        case SDLK_V:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_Paste(edit);
+                return true;
+            }
+            break;
+
+        case SDLK_X:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_Cut(edit);
+                return true;
+            }
+            break;
+
+        case SDLK_LEFT:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_MoveCursorBeginningOfLine(edit);
+            } else {
+                EditBox_MoveCursorLeft(edit);
+            }
+            return true;
+
+        case SDLK_RIGHT:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_MoveCursorEndOfLine(edit);
+            } else {
+                EditBox_MoveCursorRight(edit);
+            }
+            return true;
+
+        case SDLK_UP:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_MoveCursorBeginning(edit);
+            } else {
+                EditBox_MoveCursorUp(edit);
+            }
+            return true;
+
+        case SDLK_DOWN:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_MoveCursorEnd(edit);
+            } else {
+                EditBox_MoveCursorDown(edit);
+            }
+            return true;
+
+        case SDLK_HOME:
+            EditBox_MoveCursorBeginning(edit);
+            return true;
+
+        case SDLK_END:
+            EditBox_MoveCursorEnd(edit);
+            return true;
+
+        case SDLK_BACKSPACE:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_BackspaceToBeginning(edit);
+            } else {
+                EditBox_Backspace(edit);
+            }
+            return true;
+
+        case SDLK_DELETE:
+            if (event->key.mod & SDL_KMOD_CTRL) {
+                EditBox_DeleteToEnd(edit);
+            } else {
+                EditBox_Delete(edit);
+            }
+            return true;
+
+        default:
+            break;
+        }
+        break;
+
+    case SDL_EVENT_TEXT_INPUT:
+        EditBox_Insert(edit, event->text.text);
+        return true;
+
+    default:
+        break;
+    }
+    return false;
+}
+
diff --git a/examples/editbox.h b/examples/editbox.h
new file mode 100644
index 00000000..cb4fdc46
--- /dev/null
+++ b/examples/editbox.h
@@ -0,0 +1,53 @@
+/*
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+#include <SDL3/SDL.h>
+#include <SDL3_ttf/SDL_ttf.h>
+
+
+typedef struct EditBox {
+    TTF_Font *font;
+    TTF_Text *text;
+    SDL_FRect rect;
+    int cursor;
+    bool cursor_visible;
+    Uint64 last_cursor_change;
+    bool highlighting;
+    int highlight1, highlight2;
+
+    // Used for testing the software rendering implementation
+    SDL_Surface *window_surface;
+} EditBox;
+
+
+extern EditBox *EditBox_Create(TTF_Text *text, const SDL_FRect *rect);
+extern void EditBox_Destroy(EditBox *edit);
+extern void EditBox_Draw(EditBox *edit, SDL_Renderer *renderer);
+extern void EditBox_MoveCursorLeft(EditBox *edit);
+extern void EditBox_MoveCursorRight(EditBox *edit);
+extern void EditBox_MoveCursorUp(EditBox *edit);
+extern void EditBox_MoveCursorDown(EditBox *edit);
+extern void EditBox_MoveCursorBeginningOfLine(EditBox *edit);
+extern void EditBox_MoveCursorEndOfLine(EditBox *edit);
+extern void EditBox_MoveCursorBeginning(EditBox *edit);
+extern void EditBox_MoveCursorEnd(EditBox *edit);
+extern void EditBox_Backspace(EditBox *edit);
+extern void EditBox_BackspaceToBeginning(EditBox *edit);
+extern void EditBox_DeleteToEnd(EditBox *edit);
+extern void EditBox_Delete(EditBox *edit);
+extern void EditBox_SelectAll(EditBox *edit);
+extern bool EditBox_DeleteHighlight(EditBox *edit);
+extern void EditBox_Copy(EditBox *edit);
+extern void EditBox_Cut(EditBox *edit);
+extern void EditBox_Paste(EditBox *edit);
+extern void EditBox_Insert(EditBox *edit, const char *text);
+extern bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event);
+
diff --git a/examples/showfont.c b/examples/showfont.c
index cf87685c..20e0c837 100644
--- a/examples/showfont.c
+++ b/examples/showfont.c
@@ -26,8 +26,8 @@
 #include <SDL3_ttf/SDL_ttf.h>
 
 #include <stdlib.h>
-#include <stdio.h>
-#include <string.h>
+
+#include "editbox.h"
 
 #define DEFAULT_PTSIZE  18.0f
 #define DEFAULT_TEXT    "The quick brown fox jumped over the lazy dog"
@@ -37,7 +37,6 @@
 //#define DEFAULT_TEXT    "\xe5\xad\xa6\xe4\xb9\xa0\xe6\x9f\x90\xe8\xaf\xbe\xe7\xa8\x8b\xe5\xbf\x85\xe8\xaf\xbb\xe7\x9a\x84"
 #define WIDTH   640
 #define HEIGHT  480
-#define CURSOR_BLINK_INTERVAL_MS    500
 
 
 #define TTF_SHOWFONT_USAGE \
@@ -69,25 +68,9 @@ typedef struct {
     TTF_Text *text;
     SDL_FRect textRect;
     bool textFocus;
-    int cursor;
-    bool cursorVisible;
-    Uint64 lastCursorChange;
-    bool highlighting;
-    int highlight1, highlight2;
+    EditBox *edit;
 } Scene;
 
-static bool GetHighlightExtents(Scene *scene, int *marker1, int *marker2)
-{
-    if (scene->highlight1 >= 0 && scene->highlight2 >= 0) {
-        *marker1 = SDL_min(scene->highlight1, scene->highlight2);
-        *marker2 = SDL_max(scene->highlight1, scene->highlight2) - 1;
-        if (*marker2 > *marker1) {
-            return true;
-        }
-    }
-    return false;
-}
-
 static void DrawScene(Scene *scene)
 {
     SDL_Renderer *renderer = scene->renderer;
@@ -97,9 +80,6 @@ static void DrawScene(Scene *scene)
     SDL_RenderClear(renderer);
 
     if (scene->text) {
-        float x = scene->textRect.x + 4.0f;
-        float y = scene->textRect.y + 4.0f;
-
         /* Clear the text rect to light gray */
         SDL_SetRenderDrawColor(renderer, 0xCC, 0xCC, 0xCC, 0xFF);
         SDL_RenderFillRect(renderer, &scene->textRect);
@@ -114,60 +94,7 @@ static void DrawScene(Scene *scene)
             SDL_RenderRect(renderer, &focusRect);
         }
 
-        int marker1, marker2;
-        if (GetHighlightExtents(scene, &marker1, &marker2)) {
-            TTF_SubString **highlights = TTF_GetTextSubStringsForRange(scene->text, marker1, marker2, NULL);
-            if (highlights) {
-                int i;
-                SDL_SetRenderDrawColor(renderer, 0xCC, 0xCC, 0x00, 0xFF);
-                for (i = 0; highlights[i]; ++i) {
-                    SDL_FRect rect;
-                    SDL_RectToFRect(&highlights[i]->rect, &rect);
-                    rect.x += x;
-                    rect.y += y;
-                    SDL_RenderFillRect(renderer, &rect);
-                }
-                SDL_free(highlights);
-            }
-        }
-
-        switch (scene->textEngine) {
-        case TextEngineSurface:
-            /* Flush the renderer so we can draw directly to the window surface */
-            SDL_FlushRenderer(renderer);
-            TTF_DrawSurfaceText(scene->text, (int)x, (int)y, scene->window_surface);
-            break;
-        case TextEngineRenderer:
-            TTF_DrawRendererText(scene->text, x, y);
-            break;
-        default:
-            break;
-        }
-
-        if (scene->textFocus) {
-            /* Draw the cursor */
-            Uint64 now = SDL_GetTicks();
-            if ((now - scene->lastCursorChange) >= CURSOR_BLINK_INTERVAL_MS) {
-                scene->cursorVisible = !scene->cursorVisible;
-                scene->lastCursorChange = now;
-            }
-
-            TTF_SubString cursor;
-            if (scene->cursorVisible && TTF_GetTextSubString(scene->text, scene->cursor, &cursor)) {
-                SDL_FRect cursorRect;
-                if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_RTL) {
-                    cursorRect.x = x + cursor.rect.x + cursor.rect.w;
-                } else {
-                    cursorRect.x = x + cursor.rect.x;
-                }
-                cursorRect.y = y + cursor.rect.y;
-                cursorRect.w = 1.0f;
-                cursorRect.h = (float)cursor.rect.h;
-
-                SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF);
-                SDL_RenderFillRect(renderer, &cursorRect);
-            }
-        }
+        EditBox_Draw(scene->edit, renderer);
     }
 
     SDL_RenderTexture(renderer, scene->caption, NULL, &scene->captionRect);
@@ -179,95 +106,6 @@ static void DrawScene(Scene *scene)
     }
 }
 
-static int GetCursorTextIndex(TTF_Font *font, int x, const TTF_SubString *substring)
-{
-    bool round_down;
-    if (TTF_GetFontDirection(font) == TTF_DIRECTION_RTL) {
-        round_down = (x > (substring->rect.x + substring->rect.w / 2));
-    } else {
-        round_down = (x < (substring->rect.x + substring->rect.w / 2));
-    }
-    if (round_down) {
-        /* Start the cursor before the selected text */
-        return substring->offset;
-    } else {
-        /* Place the cursor after the selected text */
-        return substring->offset + substring->length;
-    }
-}
-
-static void MoveCursorIndex(Scene *scene, int direction)
-{
-    TTF_SubString substring;
-
-    if (direction < 0) {
-        if (TTF_GetTextSubString(scene->text, scene->cursor - 1, &substring)) {
-            scene->cursor = substring.offset;
-        }
-    } else {
-        if (TTF_GetTextSubString(scene->text, scene->cursor, &substring) &&
-            TTF_GetTextSubString(scene->text, substring.offset + substring.length, &substring)) {
-            scene->cursor = substring.offset;
-        }
-    }
-}
-
-static void MoveCursorLeft(Scene *scene)
-{
-    if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_RTL) {
-        MoveCursorIndex(scene, 1);
-    } else {
-        MoveCursorIndex(scene, -1);
-    }
-}
-
-static void MoveCursorRight(Scene *scene)
-{
-    if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_RTL) {
-        MoveCursorIndex(scene, -1);
-    } else {
-        MoveCursorIndex(scene, 1);
-    }
-}
-
-static void MoveCursorUp(Scene *scene)
-{
-    TTF_SubString substring;
-
-    if (TTF_GetTextSubString(scene->text, scene->cursor, &substring)) {
-        int fontHeight = TTF_GetFontHeight(scene->font);
-        int x, y;
-        if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_RTL) {
-            x = substring.rect.x + substring.rect.w;
-        } else {
-            x = substring.rect.x;
-        }
-        y = substring.rect.y - fontHeight;
-        if (TTF_GetTextSubStringForPoint(scene->text, x, y, &substring)) {
-            scene->cursor = GetCursorTextIndex(scene->font, x, &substring);
-        }
-    }
-}
-
-static void MoveCursorDown(Scene *scene)
-{
-    TTF_SubString substring;
-
-    if (TTF_GetTextSubString(scene->text, scene->cursor, &substring)) {
-        int fontHeight = TTF_GetFontHeight(scene->font);
-        int x, y;
-        if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_RTL) {
-            x = substring.rect.x + substring.rect.w;
-        } else {
-            x = substring.rect.x;
-        }
-        y = substring.rect.y + substring.rect.h + fontHeight;
-        if (TTF_GetTextSubStringForPoint(scene->text, x, y, &substring)) {
-            scene->cursor = GetCursorTextIndex(scene->font, x, &substring);
-        }
-    }
-}
-
 static void SetTextFocus(Scene *scene, bool focused)
 {
     if (!scene->text) {
@@ -281,60 +119,100 @@ static void SetTextFocus(Scene *scene, bool focused)
     } else {
         SDL_StopTextInput(scene->window);
     }
-
-    /* Reset the highlight */
-    scene->highlighting = false;
-    scene->highlight1 = -1;
-    scene->highlight2 = -1;
 }
 
-static void HandleTextMouseDown(Scene *scene, float x, float y)
+static void HandleKeyDown(Scene *scene, SDL_Event *event)
 {
-    if (!scene->textFocus) {
-        SetTextFocus(scene, true);
-        return;
-    }
-
-    /* Set the cursor position */
-    TTF_SubString substring;
-    int textX = (int)SDL_roundf(x - (scene->textRect.x + 4.0f));
-    int textY = (int)SDL_roundf(y - (scene->textRect.y + 4.0f));
-    if (!TTF_GetTextSubStringForPoint(scene->text, textX, textY, &substring)) {
-        SDL_Log("Couldn't get cursor location: %s\n", SDL_GetError());
-        return;
-    }
+    int style, outline;
+
+    switch (event->key.key) {
+    case SDLK_A:
+        /* Cycle alignment */
+        switch (TTF_GetFontWrapAlignment(scene->font)) {
+        case TTF_HORIZONTAL_ALIGN_LEFT:
+            TTF_SetFontWrapAlignment(scene->font, TTF_HORIZONTAL_ALIGN_CENTER);
+            break;
+        case TTF_HORIZONTAL_ALIGN_CENTER:
+            TTF_SetFontWrapAlignment(scene->font, TTF_HORIZONTAL_ALIGN_RIGHT);
+            break;
+        case TTF_HORIZONTAL_ALIGN_RIGHT:
+            TTF_SetFontWrapAlignment(scene->font, TTF_HORIZONTAL_ALIGN_LEFT);
+            break;
+        default:
+            SDL_Log("Unknown wrap alignment: %d\n", TTF_GetFontWrapAlignment(scene->font));
+            break;
+        }
+        break;
 
-    scene->cursor = GetCursorTextIndex(scene->font, textX, &substring);
-    scene->highlighting = true;
-    scene->highlight1 = scene->cursor;
-    scene->highlight2 = -1;
-}
+    case SDLK_B:
+        /* Toggle bold style */
+        style = TTF_GetFontStyle(scene->font);
+        if (style & TTF_STYLE_BOLD) {
+            style &= ~TTF_STYLE_BOLD;
+        } else {
+            style |= TTF_STYLE_BOLD;
+        }
+        TTF_SetFontStyle(scene->font, style);
+        break;
 
-static void HandleTextMouseMotion(Scene *scene, float x, float y)
-{
-    if (!scene->highlighting) {
-        return;
-    }
+    case SDLK_I:
+        /* Toggle italic style */
+        style = TTF_GetFontStyle(scene->font);
+        if (style & TTF_STYLE_ITALIC) {
+            style &= ~TTF_STYLE_ITALIC;
+        } else {
+            style |= TTF_STYLE_ITALIC;
+        }
+        TTF_SetFontStyle(scene->font, style);
+        break;
 
-    /* Set the highlight position */
-    TTF_SubString substring;
-    int textX = (int)SDL_roundf(x - (scene->textRect.x + 4.0f));
-    int textY = (int)SDL_roundf(y - (scene->textRect.y + 4.0f));
-    if (!TTF_GetTextSubStringForPoint(scene->text, textX, textY, &substring)) {
-        SDL_Log("Couldn't get cursor location: %s\n", SDL_GetError());
-        return;
-    }
+    case SDLK_O:
+        /* Toggle scene->font outline */
+        outline = TTF_GetFontOutline(scene->font);
+        if (outline) {
+            outline = 0;
+        } else {
+            outline = 1;
+        }
+        TTF_SetFontOutline(scene->font, outline);
+        break;
 
-    scene->cursor = GetCursorTextIndex(scene->font, textX, &substring);
-    scene->highlight2 = scene->cursor;
-}
+    case SDLK_R:
+        /* Toggle layout direction */
+        if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_LTR) {
+            TTF_SetFontDirection(scene->font, TTF_DIRECTION_RTL);
+        } else if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_RTL) {
+            TTF_SetFontDirection(scene->font, TTF_DIRECTION_LTR);
+        } else if (TTF_GetFontDirection(scene->font) == TTF_DIRECTION_TT

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