995 lines
28 KiB
C
995 lines
28 KiB
C
/*
|
|
Copyright (C) 1997-2025 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 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 ((cursor.flags & TTF_SUBSTRING_DIRECTION_MASK) == 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_memcpy(dst, event->edit_candidates.candidates[i], length);
|
|
dst += length;
|
|
|
|
if (!horizontal) {
|
|
*dst++ = '\n';
|
|
}
|
|
}
|
|
*dst = '\0';
|
|
|
|
edit->candidates = TTF_CreateText(TTF_GetTextEngine(edit->text), edit->font, candidate_text, 0);
|
|
SDL_free(candidate_text);
|
|
if (edit->candidates) {
|
|
float r, g, b, a;
|
|
TTF_GetTextColorFloat(edit->text, &r, &g, &b, &a);
|
|
TTF_SetTextColorFloat(edit->candidates, r, g, b, a);
|
|
} 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 */
|
|
TTF_SubString cursor;
|
|
int offset = edit->composition_start;
|
|
if (edit->composition_cursor_length > 0) {
|
|
// Place the candidates at the active clause
|
|
offset += edit->composition_cursor;
|
|
}
|
|
if (!TTF_GetTextSubString(edit->text, offset, &cursor)) {
|
|
return;
|
|
}
|
|
|
|
SDL_GetRenderSafeArea(renderer, &safe_rect);
|
|
TTF_GetTextSize(edit->candidates, &candidates_w, &candidates_h);
|
|
candidates_rect.x = edit->rect.x + cursor.rect.x;
|
|
candidates_rect.y = edit->rect.y + cursor.rect.y + 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_roundf(window_edit_rect_min.x);
|
|
rect.y = (int)SDL_roundf(window_edit_rect_min.y);
|
|
rect.w = (int)SDL_roundf(window_edit_rect_max.x - window_edit_rect_min.x);
|
|
rect.h = (int)SDL_roundf(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->window = window;
|
|
edit->renderer = renderer;
|
|
edit->font = font;
|
|
edit->text = TTF_CreateText(engine, font, NULL, 0);
|
|
if (!edit->text) {
|
|
EditBox_Destroy(edit);
|
|
return NULL;
|
|
}
|
|
edit->rect = *rect;
|
|
edit->highlight1 = -1;
|
|
edit->highlight2 = -1;
|
|
|
|
/* Wrap the editbox text within the editbox area */
|
|
TTF_SetTextWrapWidth(edit->text, (int)SDL_floorf(rect->w));
|
|
|
|
/* Show the whitespace when wrapping, so it can be edited */
|
|
TTF_SetTextWrapWhitespaceVisible(edit->text, true);
|
|
|
|
#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;
|
|
}
|
|
|
|
void EditBox_Destroy(EditBox *edit)
|
|
{
|
|
if (!edit) {
|
|
return;
|
|
}
|
|
|
|
ClearCandidates(edit);
|
|
TTF_DestroyText(edit->text);
|
|
SDL_free(edit);
|
|
}
|
|
|
|
void EditBox_SetFocus(EditBox *edit, bool focus)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
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 marker, length;
|
|
if (GetHighlightExtents(edit, &marker, &length)) {
|
|
TTF_SubString **highlights = TTF_GetTextSubStringsForRange(edit->text, marker, length, NULL);
|
|
if (highlights) {
|
|
int i;
|
|
SDL_SetRenderDrawColor(renderer, 0xEE, 0xEE, 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);
|
|
}
|
|
}
|
|
|
|
DrawText(edit, edit->text, x, y);
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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 ((cursor.flags & TTF_SUBSTRING_DIRECTION_MASK) == 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);
|
|
|
|
UpdateTextInputArea(edit);
|
|
}
|
|
|
|
if (edit->composition_length > 0) {
|
|
DrawComposition(edit);
|
|
}
|
|
|
|
if (edit->candidates) {
|
|
DrawCandidates(edit);
|
|
}
|
|
|
|
if (edit->cursor_visible) {
|
|
DrawCursor(edit);
|
|
}
|
|
}
|
|
}
|
|
|
|
static int GetCursorTextIndex(int x, const TTF_SubString *substring)
|
|
{
|
|
if (substring->flags & (TTF_SUBSTRING_LINE_END | TTF_SUBSTRING_TEXT_END)) {
|
|
return substring->offset;
|
|
}
|
|
|
|
bool round_down;
|
|
if ((substring->flags & TTF_SUBSTRING_DIRECTION_MASK) == 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 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)) {
|
|
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)) {
|
|
SetCursorPosition(edit, substring.offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EditBox_MoveCursorLeft(EditBox *edit)
|
|
{
|
|
if (!edit) {
|
|
return;
|
|
}
|
|
|
|
TTF_SubString substring;
|
|
if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
|
|
(substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
|
|
MoveCursorIndex(edit, 1);
|
|
} else {
|
|
MoveCursorIndex(edit, -1);
|
|
}
|
|
}
|
|
|
|
void EditBox_MoveCursorRight(EditBox *edit)
|
|
{
|
|
if (!edit) {
|
|
return;
|
|
}
|
|
|
|
TTF_SubString substring;
|
|
if (TTF_GetTextSubString(edit->text, edit->cursor, &substring) &&
|
|
(substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == 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 ((substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
|
|
x = substring.rect.x + substring.rect.w - 1;
|
|
} else {
|
|
x = substring.rect.x;
|
|
}
|
|
y = substring.rect.y - fontHeight / 2;
|
|
if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
|
|
SetCursorPosition(edit, GetCursorTextIndex(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 ((substring.flags & TTF_SUBSTRING_DIRECTION_MASK) == TTF_DIRECTION_RTL) {
|
|
x = substring.rect.x + substring.rect.w - 1;
|
|
} else {
|
|
x = substring.rect.x;
|
|
}
|
|
y = substring.rect.y + substring.rect.h + fontHeight / 2;
|
|
if (TTF_GetTextSubStringForPoint(edit->text, x, y, &substring)) {
|
|
SetCursorPosition(edit, GetCursorTextIndex(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)) {
|
|
SetCursorPosition(edit, 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)) {
|
|
SetCursorPosition(edit, substring.offset + substring.length);
|
|
}
|
|
}
|
|
|
|
void EditBox_MoveCursorBeginning(EditBox *edit)
|
|
{
|
|
if (!edit) {
|
|
return;
|
|
}
|
|
|
|
/* Move to the beginning of the text */
|
|
SetCursorPosition(edit, 0);
|
|
}
|
|
|
|
void EditBox_MoveCursorEnd(EditBox *edit)
|
|
{
|
|
if (!edit) {
|
|
return;
|
|
}
|
|
|
|
/* Move to the end of the text */
|
|
if (edit->text->text) {
|
|
SetCursorPosition(edit, (int)SDL_strlen(edit->text->text));
|
|
}
|
|
}
|
|
|
|
void EditBox_Backspace(EditBox *edit)
|
|
{
|
|
if (!edit || !edit->text->text) {
|
|
return;
|
|
}
|
|
|
|
if (EditBox_DeleteHighlight(edit)) {
|
|
return;
|
|
}
|
|
|
|
if (edit->cursor > 0) {
|
|
const char *start = &edit->text->text[edit->cursor];
|
|
const char *next = start;
|
|
SDL_StepBackUTF8(edit->text->text, &next);
|
|
int length = (int)(uintptr_t)(start - next);
|
|
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);
|
|
SetCursorPosition(edit, 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)) {
|
|
if (edit->has_focus) {
|
|
EditBox_SetFocus(edit, false);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!edit->has_focus) {
|
|
EditBox_SetFocus(edit, true);
|
|
}
|
|
|
|
/* Set the cursor position */
|
|
TTF_SubString substring;
|
|
int textX = (int)SDL_roundf(x - edit->rect.x);
|
|
int textY = (int)SDL_roundf(y - edit->rect.y);
|
|
if (!TTF_GetTextSubStringForPoint(edit->text, textX, textY, &substring)) {
|
|
SDL_Log("Couldn't get cursor location: %s", SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
SetCursorPosition(edit, GetCursorTextIndex(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);
|
|
int textY = (int)SDL_roundf(y - edit->rect.y);
|
|
if (!TTF_GetTextSubStringForPoint(edit->text, textX, textY, &substring)) {
|
|
SDL_Log("Couldn't get cursor location: %s", SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
SetCursorPosition(edit, GetCursorTextIndex(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 marker, length;
|
|
if (GetHighlightExtents(edit, &marker, &length)) {
|
|
TTF_DeleteTextString(edit->text, marker, length);
|
|
SetCursorPosition(edit, marker);
|
|
edit->highlight1 = -1;
|
|
edit->highlight2 = -1;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void EditBox_Copy(EditBox *edit)
|
|
{
|
|
if (!edit || !edit->text->text) {
|
|
return;
|
|
}
|
|
|
|
int marker, length;
|
|
if (GetHighlightExtents(edit, &marker, &length)) {
|
|
char *temp = (char *)SDL_malloc(length + 1);
|
|
if (temp) {
|
|
SDL_memcpy(temp, &edit->text->text[marker], 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 marker, length;
|
|
if (GetHighlightExtents(edit, &marker, &length)) {
|
|
char *temp = (char *)SDL_malloc(length + 1);
|
|
if (temp) {
|
|
SDL_memcpy(temp, &edit->text->text[marker], length);
|
|
temp[length] = '\0';
|
|
SDL_SetClipboardText(temp);
|
|
SDL_free(temp);
|
|
}
|
|
TTF_DeleteTextString(edit->text, marker, length);
|
|
SetCursorPosition(edit, marker);
|
|
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();
|
|
EditBox_Insert(edit, text);
|
|
}
|
|
|
|
void EditBox_Insert(EditBox *edit, const char *text)
|
|
{
|
|
if (!edit || !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);
|
|
SetCursorPosition(edit, (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:
|
|
if (!edit->has_focus) {
|
|
break;
|
|
}
|
|
|
|
switch (event->key.key) {
|
|
case SDLK_A:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_SelectAll(edit);
|
|
}
|
|
break;
|
|
case SDLK_C:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_Copy(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_V:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_Paste(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_X:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_Cut(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_LEFT:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_MoveCursorBeginningOfLine(edit);
|
|
} else {
|
|
EditBox_MoveCursorLeft(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_RIGHT:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_MoveCursorEndOfLine(edit);
|
|
} else {
|
|
EditBox_MoveCursorRight(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_UP:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_MoveCursorBeginning(edit);
|
|
} else {
|
|
EditBox_MoveCursorUp(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_DOWN:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_MoveCursorEnd(edit);
|
|
} else {
|
|
EditBox_MoveCursorDown(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_HOME:
|
|
EditBox_MoveCursorBeginning(edit);
|
|
break;
|
|
|
|
case SDLK_END:
|
|
EditBox_MoveCursorEnd(edit);
|
|
break;
|
|
|
|
case SDLK_BACKSPACE:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_BackspaceToBeginning(edit);
|
|
} else {
|
|
EditBox_Backspace(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_DELETE:
|
|
if (event->key.mod & SDL_KMOD_CTRL) {
|
|
EditBox_DeleteToEnd(edit);
|
|
} else {
|
|
EditBox_Delete(edit);
|
|
}
|
|
break;
|
|
|
|
case SDLK_RETURN:
|
|
EditBox_Insert(edit, "\n");
|
|
break;
|
|
|
|
case SDLK_ESCAPE:
|
|
EditBox_SetFocus(edit, false);
|
|
break;
|
|
|
|
default:
|
|
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;
|
|
}
|
|
return false;
|
|
}
|
|
|