From 2b5db7d098325f0af0f2323368970c5d7cb9f122 Mon Sep 17 00:00:00 2001 From: Sam Lantinga 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 #include +/* 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, SDL_Event *event) TTF_SetFontSize(scene->font, ptsize - 1.0f); break; + case SDLK_ESCAPE: + scene->done = true; + break; + default: break; } @@ -245,7 +233,6 @@ int main(int argc, char *argv[]) Scene scene; float ptsize; int i; - bool done = false; SDL_Color white = { 0xFF, 0xFF, 0xFF, SDL_ALPHA_OPAQUE }; SDL_Color black = { 0x00, 0x00, 0x00, SDL_ALPHA_OPAQUE }; SDL_Color *forecol; @@ -526,67 +513,45 @@ int main(int argc, char *argv[]) scene.textRect.w = WIDTH / 2 - scene.textRect.x * 2; scene.textRect.h = scene.messageRect.y - scene.textRect.y - 16.0f; - scene.text = TTF_CreateText_Wrapped(engine, font, message, 0, (int)scene.textRect.w - 8); - if (scene.text) { - scene.text->color.r = forecol->r / 255.0f; - scene.text->color.g = forecol->g / 255.0f; - scene.text->color.b = forecol->b / 255.0f; - scene.text->color.a = forecol->a / 255.0f; - - SDL_FRect editRect = scene.textRect; - editRect.x += 4.0f; - editRect.y += 4.0f; - editRect.w -= 8.0f; - editRect.w -= 8.0f; - scene.edit = EditBox_Create(scene.text, &editRect); - if (scene.edit) { - scene.edit->window_surface = scene.window_surface; - } + SDL_FRect editRect = scene.textRect; + editRect.x += 4.0f; + editRect.y += 4.0f; + editRect.w -= 8.0f; + editRect.w -= 8.0f; + scene.edit = EditBox_Create(scene.window, scene.renderer, engine, font, &editRect); + if (scene.edit) { + scene.edit->text->color.r = forecol->r / 255.0f; + scene.edit->text->color.g = forecol->g / 255.0f; + scene.edit->text->color.b = forecol->b / 255.0f; + scene.edit->text->color.a = forecol->a / 255.0f; + + EditBox_Insert(scene.edit, message); } } /* Wait for a keystroke, and blit text on mouse press */ - while (!done) { + while (!scene.done) { while (SDL_PollEvent(&event)) { SDL_ConvertEventToRenderCoordinates(scene.renderer, &event); switch (event.type) { case SDL_EVENT_MOUSE_BUTTON_DOWN: - { - SDL_FPoint pt = { event.button.x, event.button.y }; - if (SDL_PointInRectFloat(&pt, &scene.textRect)) { - if (scene.textFocus) { - EditBox_HandleEvent(scene.edit, &event); - } else { - SetTextFocus(&scene, true); - } - } else if (scene.textFocus) { - SetTextFocus(&scene, false); - } else { - scene.messageRect.x = (event.button.x - text->w/2); - scene.messageRect.y = (event.button.y - text->h/2); - scene.messageRect.w = (float)text->w; - scene.messageRect.h = (float)text->h; - } + if (!EditBox_HandleEvent(scene.edit, &event)) { + scene.messageRect.x = (event.button.x - text->w/2); + scene.messageRect.y = (event.button.y - text->h/2); + scene.messageRect.w = (float)text->w; + scene.messageRect.h = (float)text->h; } break; case SDL_EVENT_KEY_DOWN: - if (event.key.key == SDLK_ESCAPE) { - if (scene.textFocus) { - SetTextFocus(&scene, false); - } else { - done = true; - } - } else if (scene.textFocus) { - EditBox_HandleEvent(scene.edit, &event); - } else { + if (!EditBox_HandleEvent(scene.edit, &event)) { HandleKeyDown(&scene, &event); } break; case SDL_EVENT_QUIT: - done = true; + scene.done = true; break; default: @@ -598,7 +563,6 @@ int main(int argc, char *argv[]) } SDL_DestroySurface(text); EditBox_Destroy(scene.edit); - TTF_DestroyText(scene.text); TTF_CloseFont(font); switch (scene.textEngine) { case TextEngineSurface: diff --git a/include/SDL3_ttf/SDL_ttf.h b/include/SDL3_ttf/SDL_ttf.h index fc5eada8..a16cb52d 100644 --- a/include/SDL3_ttf/SDL_ttf.h +++ b/include/SDL3_ttf/SDL_ttf.h @@ -1741,18 +1741,10 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubStringForLine(TTF_Text *text, int /** * Get the substrings of a text object that contain a range of text. * - * The smaller offset will be clamped to 0 and the larger offset will be - * clamped to the length of text minus 1. The substrings that are returned - * will include the first offset and the second offset inclusive, e.g. {0, 2} - * of "abcd" will return "abc". If the text is empty, this will return a - * single zero width substring. - * - * If an offset is negative, it will be considered as an offset from the end - * of the text, so {0, -1} would return substrings for the entire text. - * * \param text the TTF_Text to query. - * \param offset1 the first byte offset into the text string. - * \param offset2 the second byte offset into the text string. + * \param offset a byte offset into the text string. + * \param length the length of the range being queried, in bytes, or -1 for the + * remainder of the string. * \param count a pointer filled in with the number of substrings returned, * may be NULL. * \returns a NULL terminated array of substring pointers or NULL on failure; @@ -1760,7 +1752,7 @@ extern SDL_DECLSPEC bool SDLCALL TTF_GetTextSubStringForLine(TTF_Text *text, int * allocation that should be freed with SDL_free() when it is no * longer needed. */ -extern SDL_DECLSPEC TTF_SubString ** SDLCALL TTF_GetTextSubStringsForRange(TTF_Text *text, int offset1, int offset2, int *count); +extern SDL_DECLSPEC TTF_SubString ** SDLCALL TTF_GetTextSubStringsForRange(TTF_Text *text, int offset, int length, int *count); /** * Get the portion of a text string that is closest to a point. diff --git a/src/SDL_ttf.c b/src/SDL_ttf.c index 424e2857..38ac12b0 100644 --- a/src/SDL_ttf.c +++ b/src/SDL_ttf.c @@ -4210,6 +4210,31 @@ bool TTF_GetTextSize(TTF_Text *text, int *w, int *h) return true; } +bool TTF_GetPreviousSubString(TTF_Text *text, TTF_SubString *substring, TTF_SubString *previous) +{ + if (previous && previous != substring) { + SDL_zerop(previous); + } + + TTF_CHECK_POINTER("text", text, false); + TTF_CHECK_POINTER("substring", substring, false); + TTF_CHECK_POINTER("previous", previous, false); + + int num_clusters = text->internal->num_clusters; + const TTF_SubString *clusters = text->internal->clusters; + if (substring->cluster_index < 0 || substring->cluster_index >= num_clusters) { + return SDL_SetError("Cluster index out of range"); + } + if (substring->offset != clusters[substring->cluster_index].offset) { + return SDL_SetError("Stale substring"); + } + if (substring->cluster_index == 0) { + return SDL_SetError("No previous substring"); + } + SDL_copyp(previous, &clusters[substring->cluster_index - 1]); + return true; +} + bool TTF_GetTextSubString(TTF_Text *text, int offset, TTF_SubString *substring) { if (substring) { @@ -4257,15 +4282,36 @@ bool TTF_GetTextSubString(TTF_Text *text, int offset, TTF_SubString *substring) } // Do a binary search to find the cluster - const TTF_SubString *closest = NULL; int low = 0; int high = num_clusters - 1; while (low <= high) { int mid = low + (high - low) / 2; cluster = &clusters[mid]; - if (offset >= cluster->offset && offset < (cluster->offset + cluster->length)) { - closest = cluster; + if (offset >= cluster->offset && (cluster->length == 0 || offset < (cluster->offset + cluster->length))) { + // In the right ballpark, expand the substring to include related clusters + --mid; + while (mid >= 0) { + cluster = &clusters[mid]; + if (offset < cluster->offset || (cluster->length != 0 && offset >= (cluster->offset + cluster->length))) { + break; + } + --mid; + } + ++mid; + + SDL_copyp(substring, &clusters[mid]); + ++mid; + while (mid < num_clusters) { + cluster = &clusters[mid]; + if (offset < cluster->offset || (cluster->length != 0 && offset >= (cluster->offset + cluster->length))) { + break; + } + + SDL_GetRectUnion(&substring->rect, &cluster->rect, &substring->rect); + substring->length = (cluster->offset - substring->offset) + cluster->length; + ++mid; + } break; } @@ -4275,9 +4321,6 @@ bool TTF_GetTextSubString(TTF_Text *text, int offset, TTF_SubString *substring) high = mid - 1; } } - if (closest) { - SDL_copyp(substring, closest); - } return true; } @@ -4340,7 +4383,7 @@ bool TTF_GetTextSubStringForLine(TTF_Text *text, int line, TTF_SubString *substr return true; } -TTF_SubString **TTF_GetTextSubStringsForRange(TTF_Text *text, int offset1, int offset2, int *count) +TTF_SubString **TTF_GetTextSubStringsForRange(TTF_Text *text, int offset, int length, int *count) { if (count) { *count = 0; @@ -4370,32 +4413,16 @@ TTF_SubString **TTF_GetTextSubStringsForRange(TTF_Text *text, int offset1, int o return result; } - int length = (int)SDL_strlen(text->text); - if (offset1 < 0) { - offset1 = length + offset1; - if (offset1 < 0) { - offset1 = 0; - } - } else if (offset1 >= length) { - offset1 = (length - 1); - } - if (offset2 < 0) { - offset2 = length + offset2; - if (offset2 < 0) { - offset2 = 0; - } - } else if (offset2 >= length) { - offset2 = (length - 1); - } - if (offset1 > offset2) { - int tmp = offset1; - offset1 = offset2; - offset2 = tmp; + if (length < 0) { + length = (int)SDL_strlen(text->text); } TTF_SubString substring1, substring2; + int offset1 = offset; + int offset2 = offset + length; if (!TTF_GetTextSubString(text, offset1, &substring1) || - !TTF_GetTextSubString(text, offset2, &substring2)) { + !TTF_GetTextSubString(text, offset2, &substring2) || + !TTF_GetPreviousSubString(text, &substring2, &substring2)) { return NULL; } @@ -4409,6 +4436,13 @@ TTF_SubString **TTF_GetTextSubStringsForRange(TTF_Text *text, int offset1, int o result[0] = substring; result[1] = NULL; SDL_copyp(substring, &substring1); + if (length == 0) { + substring->length = 0; + if (TTF_GetFontDirection(text->internal->font) != TTF_DIRECTION_RTL) { + substring->rect.x += substring->rect.w; + } + substring->rect.w = 0; + } if (count) { *count = 1;