SDL_ttf: Added full IME support to the editbox example in showfont

From c3ebe19cb0b9628886fefe2107be70a97e6ddccc Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 6 Oct 2024 00:18:31 -0700
Subject: [PATCH] Added full IME support to the editbox example in showfont

---
 examples/editbox.c         | 539 +++++++++++++++++++++++++++++++------
 examples/editbox.h         |  38 ++-
 examples/showfont.c        |  94 ++-----
 include/SDL3_ttf/SDL_ttf.h |  16 +-
 src/SDL_ttf.c              |  92 +++++--
 5 files changed, 587 insertions(+), 192 deletions(-)

diff --git a/examples/editbox.c b/examples/editbox.c
index 2d8ec847..b2d68970 100644
--- a/examples/editbox.c
+++ b/examples/editbox.c
@@ -14,19 +14,336 @@
 #define CURSOR_BLINK_INTERVAL_MS    500
 
 
-EditBox *EditBox_Create(TTF_Text *text, const SDL_FRect *rect)
+static void DrawText(EditBox *edit, TTF_Text *text, float x, float y)
+{
+#ifdef TEST_SURFACE_ENGINE
+    if (edit->window_surface) {
+        /* Flush the renderer so we can draw directly to the window surface */
+        SDL_FlushRenderer(edit->renderer);
+        TTF_DrawSurfaceText(text, (int)SDL_roundf(x), (int)SDL_roundf(y), edit->window_surface);
+        return;
+    }
+#endif /* TEST_SURFACE_ENGINE */
+
+    TTF_DrawRendererText(text, x, y);
+}
+
+static bool GetHighlightExtents(EditBox *edit, int *marker, int *length)
+{
+    if (edit->highlight1 >= 0 && edit->highlight2 >= 0) {
+        int marker1 = SDL_min(edit->highlight1, edit->highlight2);
+        int marker2 = SDL_max(edit->highlight1, edit->highlight2);
+        if (marker2 > marker1) {
+            *marker = marker1;
+            *length = marker2 - marker1;
+            return true;
+        }
+    }
+    return false;
+}
+
+static void ResetComposition(EditBox *edit)
+{
+    edit->composition_start = 0;
+    edit->composition_length = 0;
+    edit->composition_cursor = 0;
+    edit->composition_cursor_length = 0;
+}
+
+static int UTF8ByteLength(const char *text, int num_codepoints)
+{
+    const char *start = text;
+    while (num_codepoints > 0) {
+        Uint32 ch = SDL_StepUTF8(&text, NULL);
+        if (ch == 0) {
+            break;
+        }
+        --num_codepoints;
+    }
+    return (int)(uintptr_t)(text - start);
+}
+
+static void HandleComposition(EditBox *edit, const SDL_TextEditingEvent *event)
+{
+    EditBox_DeleteHighlight(edit);
+
+    if (edit->composition_length > 0) {
+        TTF_DeleteTextString(edit->text, edit->composition_start, edit->composition_length);
+        ResetComposition(edit);
+    }
+
+    int length = (int)SDL_strlen(event->text);
+    if (length > 0) {
+        edit->composition_start = edit->cursor;
+        edit->composition_length = length;
+        TTF_InsertTextString(edit->text, edit->composition_start, event->text, edit->composition_length);
+        if (event->start > 0 || event->length > 0) {
+            edit->composition_cursor = UTF8ByteLength(&edit->text->text[edit->composition_start], event->start);
+            edit->composition_cursor_length = UTF8ByteLength(&edit->text->text[edit->composition_start + edit->composition_cursor], event->length);
+        } else {
+            edit->composition_cursor = length;
+            edit->composition_cursor_length = 0;
+        }
+    }
+}
+
+static void CancelComposition(EditBox *edit)
+{
+    ResetComposition(edit);
+
+    SDL_ClearComposition(edit->window);
+}
+
+static void DrawComposition(EditBox *edit)
+{
+    /* Draw an underline under the composed text */
+    SDL_Renderer *renderer = edit->renderer;
+    int font_height = TTF_GetFontHeight(edit->font);
+    TTF_SubString **substrings = TTF_GetTextSubStringsForRange(edit->text, edit->composition_start, edit->composition_length, NULL);
+    if (substrings) {
+        for (int i = 0; substrings[i]; ++i) {
+            SDL_FRect rect;
+            SDL_RectToFRect(&substrings[i]->rect, &rect);
+            rect.x += edit->rect.x;
+            rect.y += (edit->rect.y + font_height);
+            rect.h = 1.0f;
+            SDL_RenderFillRect(renderer, &rect);
+        }
+        SDL_free(substrings);
+    }
+
+    /* Thicken the underline under the active clause in the composed text */
+    if (edit->composition_cursor_length > 0) {
+        substrings = TTF_GetTextSubStringsForRange(edit->text, edit->composition_start + edit->composition_cursor, edit->composition_cursor_length, NULL);
+        if (substrings) {
+            for (int i = 0; substrings[i]; ++i) {
+                SDL_FRect rect;
+                SDL_RectToFRect(&substrings[i]->rect, &rect);
+                rect.x += edit->rect.x;
+                rect.y += (edit->rect.y + font_height) - 1;
+                rect.h = 1.0f;
+                SDL_RenderFillRect(renderer, &rect);
+            }
+            SDL_free(substrings);
+        }
+    }
+}
+
+static void DrawCompositionCursor(EditBox *edit)
+{
+    SDL_Renderer *renderer = edit->renderer;
+    if (edit->composition_cursor_length == 0) {
+        TTF_SubString cursor;
+        if (TTF_GetTextSubString(edit->text, edit->composition_start + edit->composition_cursor, &cursor)) {
+            SDL_FRect rect;
+
+            SDL_RectToFRect(&cursor.rect, &rect);
+            if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+                rect.x += cursor.rect.w;
+            }
+            rect.x += edit->rect.x;
+            rect.y += edit->rect.y;
+            rect.w = 1.0f;
+
+            SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF);
+            SDL_RenderFillRect(renderer, &rect);
+        }
+    }
+}
+
+static void ClearCandidates(EditBox *edit)
+{
+    if (edit->candidates) {
+        TTF_DestroyText(edit->candidates);
+        edit->candidates = NULL;
+    }
+    edit->selected_candidate_start = 0;
+    edit->selected_candidate_length = 0;
+}
+
+static void SaveCandidates(EditBox *edit, const SDL_Event *event)
+{
+    int i;
+
+    ClearCandidates(edit);
+
+    bool horizontal = event->edit_candidates.horizontal;
+    int num_candidates = event->edit_candidates.num_candidates;
+    int selected_candidate = event->edit_candidates.selected_candidate;
+
+    /* Calculate the length of the candidates text */
+    size_t length = 0;
+    for (i = 0; i < num_candidates; ++i) {
+        if (horizontal) {
+            if (i > 0) {
+                ++length;
+            }
+        }
+
+        length += SDL_strlen(event->edit_candidates.candidates[i]);
+
+        if (!horizontal) {
+            length += 1;
+        }
+    }
+    if (length == 0) {
+        return;
+    }
+    ++length; /* For null terminator */
+
+    char *candidate_text = (char *)SDL_malloc(length);
+    if (!candidate_text) {
+        return;
+    }
+
+    char *dst = candidate_text;
+    for (i = 0; i < num_candidates; ++i) {
+        if (horizontal) {
+            if (i > 0) {
+                *dst++ = ' ';
+            }
+        }
+
+        int length = (int)SDL_strlen(event->edit_candidates.candidates[i]);
+        if (i == selected_candidate) {
+            edit->selected_candidate_start = (int)(uintptr_t)(dst - candidate_text);
+            edit->selected_candidate_length = length;
+            SDL_Log("Selected candidate: %d/%d\n", edit->selected_candidate_start, edit->selected_candidate_length);
+        }
+        SDL_memcpy(dst, event->edit_candidates.candidates[i], length);
+        dst += length;
+
+        if (!horizontal) {
+            *dst++ = '\n';
+        }
+    }
+    *dst = '\0';
+
+    edit->candidates = TTF_CreateText_Wrapped(TTF_GetTextEngine(edit->text), edit->font, candidate_text, 0, 0);
+    SDL_free(candidate_text);
+    if (edit->candidates) {
+        SDL_copyp(&edit->candidates->color, &edit->text->color);
+    } else {
+        ClearCandidates(edit);
+    }
+}
+
+static void DrawCandidates(EditBox *edit)
+{
+    SDL_Renderer *renderer = edit->renderer;
+    SDL_Rect safe_rect;
+    SDL_FRect candidates_rect;
+    int candidates_w;
+    int candidates_h;
+    float x, y;
+
+    /* Position the candidate window */
+    SDL_GetRenderSafeArea(renderer, &safe_rect);
+    TTF_GetTextSize(edit->candidates, &candidates_w, &candidates_h);
+    candidates_rect.x = edit->cursor_rect.x;
+    candidates_rect.y = edit->cursor_rect.y + edit->cursor_rect.h + 2.0f;
+    candidates_rect.w = 1.0f + 2.0f + candidates_w + 2.0f + 1.0f;
+    candidates_rect.h = 1.0f + 2.0f + candidates_h + 2.0f + 1.0f;
+    if ((candidates_rect.x + candidates_rect.w) > safe_rect.w) {
+        candidates_rect.x = (safe_rect.w - candidates_rect.w);
+        if (candidates_rect.x < 0.0f) {
+            candidates_rect.x = 0.0f;
+        }
+    }
+
+    /* Draw the candidate background */
+    SDL_SetRenderDrawColor(renderer, 0xAA, 0xAA, 0xAA, 0xFF);
+    SDL_RenderFillRect(renderer, &candidates_rect);
+    SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
+    SDL_RenderRect(renderer, &candidates_rect);
+
+    /* Draw the candidates */
+    x = candidates_rect.x + 3.0f;
+    y = candidates_rect.y + 3.0f;
+    DrawText(edit, edit->candidates, x, y);
+
+    /* Underline the selected candidate */
+    if (edit->selected_candidate_length > 0) {
+        int font_height = TTF_GetFontHeight(edit->font);
+        TTF_SubString **substrings = TTF_GetTextSubStringsForRange(edit->candidates, edit->selected_candidate_start, edit->selected_candidate_length, NULL);
+        if (substrings) {
+            for (int i = 0; substrings[i]; ++i) {
+                SDL_FRect rect;
+                SDL_RectToFRect(&substrings[i]->rect, &rect);
+                rect.x += x;
+                rect.y += (y + font_height);
+                rect.h = 1.0f;
+                SDL_RenderFillRect(renderer, &rect);
+            }
+            SDL_free(substrings);
+        }
+    }
+}
+
+static void UpdateTextInputArea(EditBox *edit)
+{
+    /* Convert the text input area and cursor into window coordinates */
+    SDL_Renderer *renderer = edit->renderer;
+    SDL_FPoint window_edit_rect_min;
+    SDL_FPoint window_edit_rect_max;
+    SDL_FPoint window_cursor;
+    if (!SDL_RenderCoordinatesToWindow(renderer, edit->rect.x, edit->rect.y, &window_edit_rect_min.x, &window_edit_rect_min.y) ||
+        !SDL_RenderCoordinatesToWindow(renderer, edit->rect.x + edit->rect.w, edit->rect.y + edit->rect.h, &window_edit_rect_max.x, &window_edit_rect_max.y) ||
+        !SDL_RenderCoordinatesToWindow(renderer, edit->cursor_rect.x, edit->cursor_rect.y, &window_cursor.x, &window_cursor.y)) {
+        return;
+    }
+
+    SDL_Rect rect;
+    rect.x = (int)SDL_floorf(window_edit_rect_min.x);
+    rect.y = (int)SDL_floorf(window_edit_rect_min.y);
+    rect.w = (int)SDL_floorf(window_edit_rect_max.x - window_edit_rect_min.x);
+    rect.h = (int)SDL_floorf(window_edit_rect_max.y - window_edit_rect_min.y);
+    int cursor_offset = (int)SDL_roundf(window_cursor.x - window_edit_rect_min.x);
+    SDL_SetTextInputArea(edit->window, &rect, cursor_offset);
+}
+
+static void DrawCursor(EditBox *edit)
+{
+    if (edit->composition_length > 0) {
+        DrawCompositionCursor(edit);
+        return;
+    }
+
+    SDL_Renderer *renderer = edit->renderer;
+    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF);
+    SDL_RenderFillRect(renderer, &edit->cursor_rect);
+}
+
+EditBox *EditBox_Create(SDL_Window *window, SDL_Renderer *renderer, TTF_TextEngine *engine, TTF_Font *font, 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->window = window;
+    edit->renderer = renderer;
+    edit->font = font;
+    edit->text = TTF_CreateText_Wrapped(engine, font, NULL, 0, (int)SDL_floorf(rect->w));
+    if (!edit->text) {
+        EditBox_Destroy(edit);
+        return NULL;
+    }
     edit->rect = *rect;
     edit->highlight1 = -1;
     edit->highlight2 = -1;
 
+#ifdef TEST_SURFACE_ENGINE
+    /* Grab the window surface if we want to test the surface text engine.
+     * This isn't strictly necessary, we can still use the renderer if it's
+     * a software renderer targeting an SDL_Surface.
+     */
+    edit->window_surface = (SDL_Surface *)SDL_GetPointerProperty(SDL_GetRendererProperties(renderer), SDL_PROP_RENDERER_SURFACE_POINTER, NULL);
+#endif
+
+    /* We support rendering the composition and candidates */
+    SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition,candidates");
+
     return edit;
 }
 
@@ -36,37 +353,47 @@ void EditBox_Destroy(EditBox *edit)
         return;
     }
 
+    ClearCandidates(edit);
+    TTF_DestroyText(edit->text);
     SDL_free(edit);
 }
 
-static bool GetHighlightExtents(EditBox *edit, int *marker1, int *marker2)
+void EditBox_SetFocus(EditBox *edit, bool focus)
 {
-    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;
-        }
+    if (!edit) {
+        return;
+    }
+
+    if (edit->has_focus == focus) {
+        return;
+    }
+
+    edit->has_focus = focus;
+
+    if (edit->has_focus) {
+        SDL_StartTextInput(edit->window);
+    } else {
+        SDL_StopTextInput(edit->window);
     }
-    return false;
 }
 
-void EditBox_Draw(EditBox *edit, SDL_Renderer *renderer)
+void EditBox_Draw(EditBox *edit)
 {
     if (!edit) {
         return;
     }
 
+    SDL_Renderer *renderer = edit->renderer;
     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);
+    int marker, length;
+    if (GetHighlightExtents(edit, &marker, &length)) {
+        TTF_SubString **highlights = TTF_GetTextSubStringsForRange(edit->text, marker, length, NULL);
         if (highlights) {
             int i;
-            SDL_SetRenderDrawColor(renderer, 0xCC, 0xCC, 0x00, 0xFF);
+            SDL_SetRenderDrawColor(renderer, 0xEE, 0xEE, 0x00, 0xFF);
             for (i = 0; highlights[i]; ++i) {
                 SDL_FRect rect;
                 SDL_RectToFRect(&highlights[i]->rect, &rect);
@@ -78,35 +405,43 @@ void EditBox_Draw(EditBox *edit, SDL_Renderer *renderer)
         }
     }
 
-    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);
-    }
+    DrawText(edit, 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;
-    }
+    if (edit->has_focus) {
+        /* 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;
+        /* Calculate the cursor rect, used for positioning candidates */
+        TTF_SubString cursor;
+        if (TTF_GetTextSubString(edit->text, edit->cursor, &cursor)) {
+            SDL_FRect cursor_rect;
+            SDL_RectToFRect(&cursor.rect, &cursor_rect);
+            if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
+                cursor_rect.x += cursor.rect.w;
+            }
+            cursor_rect.x += edit->rect.x;
+            cursor_rect.y += edit->rect.y;
+            cursor_rect.w = 1.0f;
+            SDL_copyp(&edit->cursor_rect, &cursor_rect);
 
-        SDL_RectToFRect(&cursor.rect, &cursorRect);
-        if (TTF_GetFontDirection(edit->font) == TTF_DIRECTION_RTL) {
-            cursorRect.x += cursor.rect.w;
+            UpdateTextInputArea(edit);
+        }
+
+        if (edit->composition_length > 0) {
+            DrawComposition(edit);
         }
-        cursorRect.x += x;
-        cursorRect.y += y;
-        cursorRect.w = 1.0f;
 
-        SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0xFF);
-        SDL_RenderFillRect(renderer, &cursorRect);
+        if (edit->candidates) {
+            DrawCandidates(edit);
+        }
+
+        if (edit->cursor_visible) {
+            DrawCursor(edit);
+        }
     }
 }
 
@@ -127,18 +462,32 @@ static int GetCursorTextIndex(TTF_Font *font, int x, const TTF_SubString *substr
     }
 }
 
+static void SetCursorPosition(EditBox *edit, int position)
+{
+    if (edit->composition_length > 0) {
+        /* Don't let the cursor be moved into the composition */
+        if (position >= edit->composition_start && position <= (edit->composition_start + edit->composition_length)) {
+            return;
+        }
+
+        CancelComposition(edit);
+    }
+
+    edit->cursor = position;
+}
+
 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;
+            SetCursorPosition(edit, substring.offset);
         }
     } else {
         if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
             TTF_GetTextSubString(edit->text, substring.offset + SDL_max(substring.length, 1), &substring)) {
-            edit->cursor = substring.offset;
+            SetCursorPosition(edit, substring.offset);
         }
     }
 }
@@ -186,7 +535,7 @@ void EditBox_MoveCursorUp(EditBox *edit)
         }
         y = substring.rect.y - fontHeight;
         if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
-            edit->cursor = GetCursorTextIndex(edit->font, x, &substring);
+            SetCursorPosition(edit, GetCursorTextIndex(edit->font, x, &substring));
         }
     }
 }
@@ -208,7 +557,7 @@ void EditBox_MoveCursorDown(EditBox *edit)
         }
         y = substring.rect.y + substring.rect.h + fontHeight;
         if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
-            edit->cursor = GetCursorTextIndex(edit->font, x, &substring);
+            SetCursorPosition(edit, GetCursorTextIndex(edit->font, x, &substring));
         }
     }
 }
@@ -222,7 +571,7 @@ void EditBox_MoveCursorBeginningOfLine(EditBox *edit)
     TTF_SubString substring;
     if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
         TTF_GetTextSubStringForLine(edit->text, substring.line_index, &substring)) {
-        edit->cursor = substring.offset;
+        SetCursorPosition(edit, substring.offset);
     }
 }
 
@@ -235,7 +584,7 @@ void EditBox_MoveCursorEndOfLine(EditBox *edit)
     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;
+        SetCursorPosition(edit, substring.offset + substring.length);
     }
 }
 
@@ -246,7 +595,7 @@ void EditBox_MoveCursorBeginning(EditBox *edit)
     }
 
     /* Move to the beginning of the text */
-    edit->cursor = 0;
+    SetCursorPosition(edit, 0);
 }
 
 void EditBox_MoveCursorEnd(EditBox *edit)
@@ -257,7 +606,7 @@ void EditBox_MoveCursorEnd(EditBox *edit)
 
     /* Move to the end of the text */
     if (edit->text->text) {
-        edit->cursor = (int)SDL_strlen(edit->text->text);
+        SetCursorPosition(edit, (int)SDL_strlen(edit->text->text));
     }
 }
 
@@ -294,7 +643,7 @@ void EditBox_BackspaceToBeginning(EditBox *edit)
 
     /* Delete to the beginning of the string */
     TTF_DeleteTextString(edit->text, 0, edit->cursor);
-    edit->cursor = 0;
+    SetCursorPosition(edit, 0);
 }
 
 void EditBox_DeleteToEnd(EditBox *edit)
@@ -329,9 +678,18 @@ static bool HandleMouseDown(EditBox *edit, float x, float y)
 {
     SDL_FPoint pt = { x, y };
     if (!SDL_PointInRectFloat(&pt, &edit->rect)) {
+        if (edit->has_focus) {
+            EditBox_SetFocus(edit, false);
+            return true;
+        }
         return false;
     }
 
+    if (!edit->has_focus) {
+        EditBox_SetFocus(edit, true);
+        return true;
+    }
+
     /* Set the cursor position */
     TTF_SubString substring;
     int textX = (int)SDL_roundf(x - (edit->rect.x + 4.0f));
@@ -341,7 +699,7 @@ static bool HandleMouseDown(EditBox *edit, float x, float y)
         return false;
     }
 
-    edit->cursor = GetCursorTextIndex(edit->font, textX, &substring);
+    SetCursorPosition(edit, GetCursorTextIndex(edit->font, textX, &substring));
     edit->highlighting = true;
     edit->highlight1 = edit->cursor;
     edit->highlight2 = -1;
@@ -364,7 +722,7 @@ static bool HandleMouseMotion(EditBox *edit, float x, float y)
         return false;
     }
 
-    edit->cursor = GetCursorTextIndex(edit->font, textX, &substring);
+    SetCursorPosition(edit, GetCursorTextIndex(edit->font, textX, &substring));
     edit->highlight2 = edit->cursor;
 
     return true;
@@ -398,11 +756,10 @@ bool EditBox_DeleteHighlight(EditBox *edit)
         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;
+    int marker, length;
+    if (GetHighlightExtents(edit, &marker, &length)) {
+        TTF_DeleteTextString(edit->text, marker, length);
+        SetCursorPosition(edit, marker);
         edit->highlight1 = -1;
         edit->highlight2 = -1;
         return true;
@@ -416,12 +773,11 @@ void EditBox_Copy(EditBox *edit)
         return;
     }
 
-    int marker1, marker2;
-    if (GetHighlightExtents(edit, &marker1, &marker2)) {
-        size_t length = marker2 - marker1 + 1;
+    int marker, length;
+    if (GetHighlightExtents(edit, &marker, &length)) {
         char *temp = (char *)SDL_malloc(length + 1);
         if (temp) {
-            SDL_memcpy(temp, &edit->text->text[marker1], length);
+            SDL_memcpy(temp, &edit->text->text[marker], length);
             temp[length] = '\0';
             SDL_SetClipboardText(temp);
             SDL_free(temp);
@@ -438,18 +794,17 @@ void EditBox_Cut(EditBox *edit)
     }
 
     /* Copy to clipboard and delete text */
-    int marker1, marker2;
-    if (GetHighlightExtents(edit, &marker1, &marker2)) {
-        size_t length = marker2 - marker1 + 1;
+    int marker, length;
+    if (GetHighlightExtents(edit, &marker, &length)) {
         char *temp = (char *)SDL_malloc(length + 1);
         if (temp) {
-            SDL_memcpy(temp, &edit->text->text[marker1], length);
+            SDL_memcpy(temp, &edit->text->text[marker], length);
             temp[length] = '\0';
-            SDL_SetClipboardText(edit->text->text);
+            SDL_SetClipboardText(temp);
             SDL_free(temp);
         }
-        TTF_DeleteTextString(edit->text, marker1, (int)length);
-        edit->cursor = marker1;
+        TTF_DeleteTextString(edit->text, marker, length);
+        SetCursorPosition(edit, marker);
         edit->highlight1 = -1;
         edit->highlight2 = -1;
     } else {
@@ -467,7 +822,7 @@ void EditBox_Paste(EditBox *edit)
     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);
+    SetCursorPosition(edit, (int)(edit->cursor + length));
 }
 
 void EditBox_Insert(EditBox *edit, const char *text)
@@ -476,9 +831,16 @@ void EditBox_Insert(EditBox *edit, const char *text)
         return;
     }
 
+    EditBox_DeleteHighlight(edit);
+
+    if (edit->composition_length > 0) {
+        TTF_DeleteTextString(edit->text, edit->composition_start, edit->composition_length);
+        edit->composition_length = 0;
+    }
+
     size_t length = SDL_strlen(text);
     TTF_InsertTextString(edit->text, edit->cursor, text, length);
-    edit->cursor = (int)(edit->cursor + length);
+    SetCursorPosition(edit, (int)(edit->cursor + length));
 }
 
 bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
@@ -498,31 +860,31 @@ bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
         return HandleMouseUp(edit, event->button.x, event->button.y);
 
     case SDL_EVENT_KEY_DOWN:
+        if (!edit->has_focus) {
+            break;
+        }
+
         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;
 
@@ -532,7 +894,7 @@ bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
             } else {
                 EditBox_MoveCursorLeft(edit);
             }
-            return true;
+            break;
 
         case SDLK_RIGHT:
             if (event->key.mod & SDL_KMOD_CTRL) {
@@ -540,7 +902,7 @@ bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
             } else {
                 EditBox_MoveCursorRight(edit);
             }
-            return true;
+            break;
 
         case SDLK_UP:
             if (event->key.mod & SDL_KMOD_CTRL) {
@@ -548,7 +910,7 @@ bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
             } else {
                 EditBox_MoveCursorUp(edit);
             }
-            return true;
+            break;
 
         case SDLK_DOWN:
             if (event->key.mod & SDL_KMOD_CTRL) {
@@ -556,15 +918,15 @@ bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
             } else {
                 EditBox_MoveCursorDown(edit);
             }
-            return true;
+            break;
 
         case SDLK_HOME:
             EditBox_MoveCursorBeginning(edit);
-            return true;
+            break;
 
         case SDLK_END:
             EditBox_MoveCursorEnd(edit);
-            return true;
+            break;
 
         case SDLK_BACKSPACE:
             if (event->key.mod & SDL_KMOD_CTRL) {
@@ -572,7 +934,7 @@ bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
             } else {
                 EditBox_Backspace(edit);
             }
-            return true;
+            break;
 
         case SDLK_DELETE:
             if (event->key.mod & SDL_KMOD_CTRL) {
@@ -580,17 +942,30 @@ bool EditBox_HandleEvent(EditBox *edit, SDL_Event *event)
             } else {
                 EditBox_Delete(edit);
             }
-            return true;
+            break;
+
+        case SDLK_ESCAPE:
+            EditBox_SetFocus(edit, false);
+            break;
 
         default:
             break;
         }
-        break;
+        return true;
 
     case SDL_EVENT_TEXT_INPUT:
         EditBox_Insert(edit, event->text.text);
         return true;
 
+    case SDL_EVENT_TEXT_EDITING:
+        HandleComposition(edit, &event->edit);
+        break;
+
+    case SDL_EVENT_TEXT_EDITING_CANDIDATES:
+        ClearCandidates(edit);
+        SaveCandidates(edit, event);
+        break;
+
     default:
         break;
     }
diff --git a/examples/editbox.h b/examples/editbox.h
index cb4fdc46..80fdd708 100644
--- a/examples/editbox.h
+++ b/examples/editbox.h
@@ -9,28 +9,58 @@
   including commercial applications, and to alter it and redistribute it
   freely.
 */
+
+/* This is an example of using SDL_ttf to create a multi-line editbox
+ * with full IME support.
+ */
+
 #include <SDL3/SDL.h>
 #include <SDL3_ttf/SDL_ttf.h>
 
+/* Define this if you want to test the surface text engine */
+#define TEST_SURFACE_ENGINE
 
 typedef struct EditBox {
+    SDL_Window *window;
+    SDL_Renderer *renderer;
     TTF_Font *font;
     TTF_Text *text;
     SDL_FRect rect;
+    bool has_focus;
+
+    /* Cursor support */
     int cursor;
+    int cursor_length;
     bool cursor_visible;
     Uint64 last_cursor_change;
+    SDL_FRect cursor_rect;
+
+    /* Highlight support */
     bool highlighting;
-    int highlight1, highlight2;
+    int highlight1;
+    int highlight2;
+
+    /* IME composition */
+    int composition_start;
+    int composition_length;
+    int composition_cursor;
+    int composition_cursor_length;
+
+    /* IME candidates */
+    TTF_Text *candidates;
+    int selected_candidate_start;
+    int selected_candidate_length;
 
-    // Used for testing the software rendering implementation
+#ifdef TEST_SURFACE_ENGINE
     SDL_Surface *window_surface;
+#endif
 } EditBox;
 
 
-extern EditBox *EditBox_Create(TTF_Text *text, const SDL_FRect *rect);
+extern EditBox *EditBox_Create(SDL_Window *window, SDL_Renderer *renderer, TTF_TextEngine *engine, TTF_Font *font, const SDL_FRect *rect);
 extern void EditBox_Destroy(EditBox *edit);
-extern void EditBox_Draw(EditBox *edit, SDL_Renderer *renderer);
+extern void EditBox_SetFocus(EditBox *edit, bool focus);
+extern void EditBox_Draw(EditBox *edit);
 extern void EditBox_MoveCursorLeft(EditBox *edit);
 extern void EditBox_MoveCursorRight(EditBox *edit);
 extern void EditBox_MoveCursorUp(EditBox *edit);
diff --git a/examples/showfont.c b/examples/showfont.c
index 3baddc47..fe804f49 100644
--- a/examples/showfont.c
+++ b/examples/showfont.c
@@ -56,6 +56,7 @@ typedef enum
 } TextRenderMethod;
 
 typedef struct {
+    bool done;
     SDL_Window *window;
     SDL_Surface *window_surface;
     SDL_Renderer *renderer;
@@ -65,9 +66,7 @@ typedef struct {
     SDL_Texture *message;
     SDL_FRect messageRect;
     TextEngine textEngine;
-    TTF_Text *text;
     SDL_FRect textRect;
-    bool textFocus;
     EditBox *edit;
 } Scene;
 
@@ -79,12 +78,12 @@ static void DrawScene(Scene *scene)
     SDL_SetRenderDrawColor(renderer, 0xFF, 0xFF, 0xFF, 0xFF);
     SDL_RenderClear(renderer);
 
-    if (scene->text) {
+    if (scene->edit) {
         /* Clear the text rect to light gray */
         SDL_SetRenderDrawColor(renderer, 0xCC, 0xCC, 0xCC, 0xFF);
         SDL_RenderFillRect(renderer, &scene->textRect);
 
-        if (scene->textFocus) {
+        if (scene->edit->has_focus) {
             SDL_FRect focusRect = scene->textRect;
             focusRect.x -= 1;
             focusRect.y -= 1;
@@ -94,7 +93,7 @@ static void DrawScene(Scene *scene)
             SDL_RenderRect(renderer, &focusRect);
         }
 
-        EditBox_Draw(scene->edit, renderer);
+        EditBox_Draw(scene->edit);
     }
 
     SDL_RenderTexture(renderer, scene->caption, NULL, &scene->captionRect);
@@ -106,21 +105,6 @@ static void DrawScene(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 HandleKeyDown(Scene *scene, SDL_Event *event)
 {
     int style, outline;
@@ -225,6 +209,10 @@ static void HandleKeyDown(Scene *scene, SD

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