diff --git a/src/menu_sdl.c b/src/menu_sdl.c index 096205a..5412395 100644 --- a/src/menu_sdl.c +++ b/src/menu_sdl.c @@ -23,6 +23,7 @@ #include #include +#include #include "portab.h" #include "system.h" @@ -32,6 +33,7 @@ #include "gfx_private.h" #include "msgskip.h" #include "modal.h" +#include "utfsjis.h" #ifdef _WIN32 #include "win/dialog.h" #endif @@ -157,11 +159,180 @@ void menu_resetmenu_open(void) { } } +// The string-input dialog. +// The input field is not a microui textbox: to support IME (SDL_TEXTEDITING) +// composition we keep our own buffer (str_buf) and feed it from the handler, +// mirroring editor.c. +struct string_state { + modal base; + INPUTSTRING_PARAM *param; + char composing[SDL_TEXTEDITINGEVENT_TEXT_SIZE]; // IME preedit text + bool done; + bool accepted; +}; + +// Heap buffer holding the string dialog's edited text. It must outlive the +// modal loop because the caller reads p->newstring after menu_inputstring() +// returns; it is freed and reallocated on the next call. +static char *str_buf; + +// Append `add` to str_buf, truncating so the total stays within +// st->param->max characters (counted as UTF-8 code points). +static void str_append(struct string_state *st, const char *add) { + int room = st->param->max; + for (const char *p = str_buf; *p; p = advance_char(p, UTF8)) + room--; + const char *end = add; + while (room > 0 && *end) { + end = advance_char(end, UTF8); + room--; + } + strncat(str_buf, add, end - add); +} + +// Delete the last UTF-8 character of str_buf. +static void str_backspace(void) { + if (!*str_buf) + return; + char *p = str_buf + strlen(str_buf) - 1; + while (p > str_buf && UTF8_TRAIL_BYTE(*p)) + p--; + *p = '\0'; +} + +static bool menu_string_handler(const SDL_Event *e, modal *modal) { + struct string_state *st = (struct string_state *)modal; + switch (e->type) { + case SDL_KEYDOWN: + // While an IME composition is active, let it consume the key. + if (!*st->composing) { + switch (e->key.keysym.sym) { + case SDLK_RETURN: + st->accepted = true; + st->done = true; + break; + case SDLK_ESCAPE: + st->base.cancelled = true; + break; + case SDLK_BACKSPACE: + str_backspace(); + break; + } + } + return true; // never forward keys to microui (there is no textbox) + case SDL_TEXTINPUT: + str_append(st, e->text.text); + st->composing[0] = '\0'; + return true; + case SDL_TEXTEDITING: + strncpy(st->composing, e->edit.text, sizeof(st->composing) - 1); + st->composing[sizeof(st->composing) - 1] = '\0'; + return true; + } + return modal_default_handler(e, modal); +} + +// Tell SDL where the text input is, so the IME candidate window is positioned +// near the field. `box` is in logical (view) coordinates; convert to window +// pixels for SDL_SetTextInputRect. +static void set_text_input_rect(mu_Rect box) { + SDL_Rect r = { box.x, box.y, box.w, box.h }; +#if SDL_VERSION_ATLEAST(2, 0, 18) + int x2, y2; + SDL_RenderLogicalToWindow(gfx_renderer, box.x, box.y, &r.x, &r.y); + SDL_RenderLogicalToWindow(gfx_renderer, box.x + box.w, box.y + box.h, &x2, &y2); + r.w = x2 - r.x; + r.h = y2 - r.y; +#endif + SDL_SetTextInputRect(&r); +} + +// Draw the IME-aware text field (committed text, preedit with underline, caret). +static void draw_string_field(mu_Context *ctx, mu_Rect box, const struct string_state *st) { + mu_draw_rect(ctx, box, ctx->style->colors[MU_COLOR_BASE]); + mu_draw_box(ctx, box, ctx->style->colors[MU_COLOR_BORDER]); + + mu_Font font = ctx->style->font; + mu_Color col = ctx->style->colors[MU_COLOR_TEXT]; + int th = ctx->text_height(font); + int x = box.x + ctx->style->padding; + int y = box.y + (box.h - th) / 2; + + mu_push_clip_rect(ctx, box); + if (*str_buf) { + mu_draw_text(ctx, font, str_buf, -1, mu_vec2(x, y), col); + x += ctx->text_width(font, str_buf, -1); + } + if (*st->composing) { + mu_draw_text(ctx, font, st->composing, -1, mu_vec2(x, y), col); + int cw = ctx->text_width(font, st->composing, -1); + mu_draw_rect(ctx, mu_rect(x, y + th, cw, 1), col); // preedit underline + x += cw; + } + mu_draw_rect(ctx, mu_rect(x, y, 1, th), col); // caret + mu_pop_clip_rect(ctx); + + set_text_input_rect(box); +} + +static bool inputstring_build(mu_Context *ctx, modal *modal) { + struct string_state *st = (struct string_state *)modal; + const char *title = (st->param->title && *st->param->title) + ? st->param->title : _("Enter a string"); + int row_h = ctx->text_height(ctx->style->font) + ctx->style->padding * 2; + int w = 320; + int h = 3 * (row_h + ctx->style->spacing) + ctx->style->padding * 2 + + ctx->style->title_height; + mu_Rect r = mu_rect((view_w - w) / 2, (view_h - h) / 2, w, h); + + if (mu_begin_window_ex(ctx, title, r, + MU_OPT_NORESIZE | MU_OPT_NOCLOSE | MU_OPT_NOSCROLL)) { + mu_layout_row(ctx, 1, (int[]){ -1 }, 0); + + char info[64]; + snprintf(info, sizeof(info), _("Up to %d characters"), st->param->max); + mu_label(ctx, info); + + draw_string_field(ctx, mu_layout_next(ctx), st); + + int content = w - ctx->style->padding * 2; + int half = (content - ctx->style->spacing) / 2; + mu_layout_row(ctx, 2, (int[]){ half, -1 }, 0); + if (mu_button(ctx, _("OK"))) { + st->accepted = true; + st->done = true; + } + if (mu_button(ctx, _("Cancel"))) { + st->accepted = false; + st->done = true; + } + mu_end_window(ctx); + } + + if (st->base.cancelled) { // Esc + st->accepted = false; + st->done = true; + } + return !st->done; +} + bool menu_inputstring(INPUTSTRING_PARAM *p) { #ifdef _WIN32 return input_string(p); #else - p->newstring = p->oldstring; + struct string_state st = { + .base = { .build = inputstring_build, .handler = menu_string_handler }, + .param = p, + }; + free(str_buf); + str_buf = malloc(p->max * MAX_UTF8_BYTES_PAR_CHAR + 1); + strcpy(str_buf, p->oldstring ? p->oldstring : ""); + + SDL_StartTextInput(); + modal_run(&st.base); + SDL_StopTextInput(); + + p->newstring = st.accepted ? str_buf : p->oldstring; return true; #endif }