menu_sdl: Implement the string-input dialog

The text field is hand-drawn and IME-aware: SDL_TEXTINPUT /
SDL_TEXTEDITING / BACKSPACE are handled directly (mirroring editor.c)
instead of using a microui textbox.
This commit is contained in:
kichikuou
2026-06-28 13:07:42 +09:00
parent 651fc21a19
commit 269132fede
+172 -1
View File
@@ -23,6 +23,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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
}