diff --git a/src/input_modal.c b/src/input_modal.c index 384407b..fb151c1 100644 --- a/src/input_modal.c +++ b/src/input_modal.c @@ -148,118 +148,31 @@ bool input_modal_number(INPUTNUM_PARAM *p) { #include "utfsjis.h" // 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; - FontSpec font; // inline edit (input_modal_string_inline) only + bool focus_init; // focus the text field on the first frame + FontSpec font; // inline edit (input_modal_string_inline) only }; -// Heap buffer holding the string dialog's edited text. It must outlive the -// modal loop because the caller reads p->newstring after input_modal_string() -// returns; it is freed and reallocated on the next call. +// Heap buffer holding the edited text. It must outlive the modal loop because +// the caller reads p->newstring after input_modal_string() returns; it is freed +// and reallocated on the next call. static char *str_buf; +static int str_bufsz; -// 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 = gfx_viewToWindowRect( - (SDL_Rect){box.x, box.y, box.w, box.h}); - SDL_SetTextInputRect(&r); +static void str_buf_init(const char *initial, int max_chars) { + free(str_buf); + str_bufsz = max_chars * MAX_UTF8_BYTES_PAR_CHAR + 1; + str_buf = malloc(str_bufsz); + strncpy(str_buf, initial ? initial : "", str_bufsz - 1); + str_buf[str_bufsz - 1] = '\0'; } #ifndef _WIN32 // input_modal_string is provided by win/dialog.c on Windows. -// 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) @@ -278,7 +191,17 @@ static bool inputstring_build(mu_Context *ctx, modal *modal) { snprintf(info, sizeof(info), _("Up to %d characters"), st->param->max); mu_label(ctx, info); - draw_string_field(ctx, mu_layout_next(ctx), st); + mu_Id id = mu_get_id(ctx, "value", 5); + mu_Rect box = mu_layout_next(ctx); + if (st->focus_init) { + mu_set_kb_focus(ctx, id); + st->focus_init = false; + } + if (mu_textbox_raw(ctx, str_buf, str_bufsz, st->param->max, id, box, 0) + & MU_RES_SUBMIT) { + st->accepted = true; + st->done = true; + } int content = w - ctx->style->padding * 2; int half = (content - ctx->style->spacing) / 2; @@ -303,16 +226,12 @@ static bool inputstring_build(mu_Context *ctx, modal *modal) { bool input_modal_string(INPUTSTRING_PARAM *p) { struct string_state st = { - .base = { .build = inputstring_build, .handler = menu_string_handler }, + .base = { .build = inputstring_build, .handler = modal_default_handler }, .param = p, + .focus_init = true, }; - free(str_buf); - str_buf = malloc(p->max * MAX_UTF8_BYTES_PAR_CHAR + 1); - strcpy(str_buf, p->oldstring ? p->oldstring : ""); - - SDL_StartTextInput(); + str_buf_init(p->oldstring, p->max); modal_run(&st.base); - SDL_StopTextInput(); p->newstring = st.accepted ? str_buf : p->oldstring; return true; @@ -320,8 +239,8 @@ bool input_modal_string(INPUTSTRING_PARAM *p) { #endif // !_WIN32 -// The inline text input (MJ command). It reuses the IME buffer handling -// and the event handler of input_modal_string. +// The inline text input (MJ command). It shares the textbox input handling but +// draws itself with the game's font and palette. // Look up an AGS color index in the active palette and return it as a mu_Color. static mu_Color palette_color(int index) { @@ -342,6 +261,17 @@ static bool inline_edit_build(mu_Context *ctx, modal *modal) { // all drawing below is done manually with the game's colors. if (mu_begin_window_ex(ctx, "editstr", rect, MU_OPT_NOFRAME | MU_OPT_NOTITLE | MU_OPT_NORESIZE | MU_OPT_NOSCROLL)) { + mu_Id id = mu_get_id(ctx, "editstr", 7); + if (st->focus_init) { + mu_set_kb_focus(ctx, id); + st->focus_init = false; + } + if (mu_textbox_input(ctx, str_buf, str_bufsz, st->param->max, id, rect, 0) + & MU_RES_SUBMIT) { + st->accepted = true; + st->done = true; + } + mu_Color bg = palette_color(nact->msg.WinBackgroundColor); mu_Color fg = palette_color(nact->msg.MsgFontColor); mu_Font font = (mu_Font)&st->font; @@ -354,14 +284,13 @@ static bool inline_edit_build(mu_Context *ctx, modal *modal) { mu_draw_text(ctx, font, str_buf, -1, mu_vec2(x, y), fg); x += ctx->text_width(font, str_buf, -1); } - if (*st->composing) { - mu_draw_text(ctx, font, st->composing, -1, mu_vec2(x, y), fg); - int cw = ctx->text_width(font, st->composing, -1); + if (*ctx->preedit) { + mu_draw_text(ctx, font, ctx->preedit, -1, mu_vec2(x, y), fg); + int cw = ctx->text_width(font, ctx->preedit, -1); mu_draw_rect(ctx, mu_rect(x, y + fh, cw, 2), fg); // preedit underline x += cw; } mu_draw_rect(ctx, mu_rect(x, y, 2, fh), fg); // caret - set_text_input_rect(rect); mu_end_window(ctx); } @@ -372,18 +301,14 @@ static bool inline_edit_build(mu_Context *ctx, modal *modal) { bool input_modal_string_inline(INPUTSTRING_PARAM *p) { struct string_state st = { - .base = { .build = inline_edit_build, .handler = menu_string_handler, + .base = { .build = inline_edit_build, .handler = modal_default_handler, .no_dim = true }, .param = p, + .focus_init = true, .font = { FONT_GOTHIC, FONT_WEIGHT_NORMAL, p->h }, }; - free(str_buf); - str_buf = malloc(p->max * MAX_UTF8_BYTES_PAR_CHAR + 1); - strcpy(str_buf, p->oldstring ? p->oldstring : ""); - - SDL_StartTextInput(); + str_buf_init(p->oldstring, p->max); modal_run(&st.base); - SDL_StopTextInput(); p->newstring = st.accepted ? str_buf : NULL; return true; @@ -456,7 +381,7 @@ static bool inputnumber_build(mu_Context *ctx, modal *modal) { mu_set_kb_focus(ctx, id); st->focus_init = false; } - int res = mu_textbox_raw(ctx, st->buf, sizeof(st->buf), id, box, 0); + int res = mu_textbox_raw(ctx, st->buf, sizeof(st->buf), 0, id, box, 0); if (mu_button(ctx, "-")) num_adjust(st, -1); if (mu_button(ctx, "+")) @@ -501,10 +426,7 @@ bool input_modal_number(INPUTNUM_PARAM *p) { .focus_init = true, }; snprintf(st.buf, sizeof(st.buf), "%d", p->def); - - SDL_StartTextInput(); modal_run(&st.base); - SDL_StopTextInput(); if (!st.accepted) return false; diff --git a/src/microui/microui.c b/src/microui/microui.c index 1887749..b53d3cd 100644 --- a/src/microui/microui.c +++ b/src/microui/microui.c @@ -149,6 +149,7 @@ void mu_begin(mu_Context *ctx) { ctx->nav_first = ctx->nav_last = 0; ctx->nav_prev = ctx->nav_next = ctx->nav_cursor = 0; ctx->kb_focus_seen = 0; + ctx->text_input = 0; ctx->frame++; } @@ -190,6 +191,9 @@ void mu_end(mu_Context *ctx) { } } + /* drop a composition left over from a textbox that lost the focus */ + if (!ctx->text_input) { ctx->preedit[0] = '\0'; } + /* bring hover root to front if mouse was pressed */ if (ctx->mouse_pressed && ctx->next_hover_root && ctx->next_hover_root->zindex < ctx->last_zindex && @@ -427,6 +431,8 @@ void mu_input_scroll(mu_Context *ctx, int x, int y) { void mu_input_keydown(mu_Context *ctx, int key) { + /* while an IME composition is active the keys belong to the IME */ + if (*ctx->preedit) { return; } ctx->key_pressed |= key; ctx->key_down |= key; } @@ -438,10 +444,22 @@ void mu_input_keyup(mu_Context *ctx, int key) { void mu_input_text(mu_Context *ctx, const char *text) { + /* SDL splits a long commit into several events, which all arrive within one + ** frame and can overflow the buffer. Drop the excess instead of failing. */ int len = strlen(ctx->input_text); - int size = strlen(text) + 1; - expect(len + size <= (int) sizeof(ctx->input_text)); - memcpy(ctx->input_text + len, text, size); + int n = mu_min((int) strlen(text), (int) sizeof(ctx->input_text) - 1 - len); + while (n > 0 && (text[n] & 0xc0) == 0x80) { n--; } /* keep utf-8 whole */ + memcpy(ctx->input_text + len, text, n); + ctx->input_text[len + n] = '\0'; + ctx->preedit[0] = '\0'; /* committing ends the composition */ +} + + +void mu_input_preedit(mu_Context *ctx, const char *text) { + int n = mu_min((int) strlen(text), (int) sizeof(ctx->preedit) - 1); + while (n > 0 && (text[n] & 0xc0) == 0x80) { n--; } /* keep utf-8 whole */ + memcpy(ctx->preedit, text, n); + ctx->preedit[n] = '\0'; } @@ -829,59 +847,91 @@ int mu_checkbox(mu_Context *ctx, const char *label, int *state) { } -int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, mu_Id id, mu_Rect r, - int opt) +/* number of characters in a utf-8 string */ +static int utf8_strlen(const char *s) { + int n = 0; + for (; *s; s++) { if ((*s & 0xc0) != 0x80) { n++; } } + return n; +} + + +int mu_textbox_input(mu_Context *ctx, char *buf, int bufsz, int max_chars, + mu_Id id, mu_Rect r, int opt) { int res = 0; - /* the keyboard focus decides which textbox is editable, so that Tab moves the - ** caret out of a MU_OPT_HOLDFOCUS textbox. A click reaches this through - ** mu_update_control(), which also sets kb_focus. */ - if (ctx->kb_focus == id) { mu_set_focus(ctx, id); } - else if (ctx->focus == id) { mu_set_focus(ctx, 0); } - mu_update_control(ctx, id, r, opt | MU_OPT_HOLDFOCUS); + /* the keyboard focus decides which textbox is editable, so that Tab moves + ** the caret out of it. A click reaches this through mu_update_control(). */ + mu_update_control(ctx, id, r, opt); + if (ctx->kb_focus != id) { return 0; } - if (ctx->focus == id) { - /* handle text input */ - int len = strlen(buf); - int n = mu_min(bufsz - len - 1, (int) strlen(ctx->input_text)); - if (n > 0) { - memcpy(buf + len, ctx->input_text, n); - len += n; - buf[len] = '\0'; - res |= MU_RES_CHANGE; - } - /* handle backspace */ - if (ctx->key_pressed & MU_KEY_BACKSPACE && len > 0) { - /* skip utf-8 continuation bytes */ - while ((buf[--len] & 0xc0) == 0x80 && len > 0); - buf[len] = '\0'; - res |= MU_RES_CHANGE; - } - /* handle return */ - if (ctx->key_pressed & MU_KEY_RETURN) { - mu_set_focus(ctx, 0); - res |= MU_RES_SUBMIT; - } + /* let the host turn the IME on and place its candidate window */ + ctx->text_input = 1; + ctx->text_input_rect = r; + + /* handle text input, honoring both the byte and the character limit */ + int len = strlen(buf); + int room = bufsz - len - 1; + int chars = max_chars ? max_chars - utf8_strlen(buf) : bufsz; + int n = 0; + while (ctx->input_text[n] && chars > 0) { + int e = n + 1; + while ((ctx->input_text[e] & 0xc0) == 0x80) { e++; } + if (e > room) { break; } + n = e; + chars--; } + if (n > 0) { + memcpy(buf + len, ctx->input_text, n); + len += n; + buf[len] = '\0'; + res |= MU_RES_CHANGE; + } + /* handle backspace */ + if (ctx->key_pressed & MU_KEY_BACKSPACE && len > 0) { + /* skip utf-8 continuation bytes */ + while ((buf[--len] & 0xc0) == 0x80 && len > 0); + buf[len] = '\0'; + res |= MU_RES_CHANGE; + } + /* handle return */ + if (ctx->key_pressed & MU_KEY_RETURN) { res |= MU_RES_SUBMIT; } - /* draw */ + return res; +} + + +void mu_draw_textbox(mu_Context *ctx, const char *buf, mu_Id id, mu_Rect r, + int opt) +{ mu_draw_control_frame(ctx, id, r, MU_COLOR_BASE, opt); - if (ctx->focus == id) { - mu_Color color = ctx->style->colors[MU_COLOR_TEXT]; - mu_Font font = ctx->style->font; - int textw = ctx->text_width(font, buf, -1); - int texth = ctx->text_height(font); - int ofx = r.w - ctx->style->padding - textw - 1; - int textx = r.x + mu_min(ofx, ctx->style->padding); - int texty = r.y + (r.h - texth) / 2; - mu_push_clip_rect(ctx, r); - mu_draw_text(ctx, font, buf, -1, mu_vec2(textx, texty), color); - mu_draw_rect(ctx, mu_rect(textx + textw, texty, 1, texth), color); - mu_pop_clip_rect(ctx); - } else { + if (ctx->kb_focus != id) { mu_draw_control_text(ctx, buf, r, MU_COLOR_TEXT, opt); + return; } + mu_Color color = ctx->style->colors[MU_COLOR_TEXT]; + mu_Font font = ctx->style->font; + int textw = ctx->text_width(font, buf, -1); + int prew = *ctx->preedit ? ctx->text_width(font, ctx->preedit, -1) : 0; + int texth = ctx->text_height(font); + int ofx = r.w - ctx->style->padding - textw - prew - 1; + int textx = r.x + mu_min(ofx, ctx->style->padding); + int texty = r.y + (r.h - texth) / 2; + mu_push_clip_rect(ctx, r); + mu_draw_text(ctx, font, buf, -1, mu_vec2(textx, texty), color); + if (prew) { + mu_draw_text(ctx, font, ctx->preedit, -1, mu_vec2(textx + textw, texty), color); + mu_draw_rect(ctx, mu_rect(textx + textw, texty + texth, prew, 1), color); + } + mu_draw_rect(ctx, mu_rect(textx + textw + prew, texty, 1, texth), color); + mu_pop_clip_rect(ctx); +} + +int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, int max_chars, + mu_Id id, mu_Rect r, int opt) +{ + int res = mu_textbox_input(ctx, buf, bufsz, max_chars, id, r, opt); + mu_draw_textbox(ctx, buf, id, r, opt); return res; } @@ -895,8 +945,8 @@ static int number_textbox(mu_Context *ctx, mu_Real *value, mu_Rect r, mu_Id id) } if (ctx->number_edit == id) { int res = mu_textbox_raw( - ctx, ctx->number_edit_buf, sizeof(ctx->number_edit_buf), id, r, 0); - if (res & MU_RES_SUBMIT || ctx->focus != id) { + ctx, ctx->number_edit_buf, sizeof(ctx->number_edit_buf), 0, id, r, 0); + if (res & MU_RES_SUBMIT || ctx->kb_focus != id) { *value = strtod(ctx->number_edit_buf, NULL); ctx->number_edit = 0; } else { @@ -910,7 +960,7 @@ static int number_textbox(mu_Context *ctx, mu_Real *value, mu_Rect r, mu_Id id) int mu_textbox_ex(mu_Context *ctx, char *buf, int bufsz, int opt) { mu_Id id = mu_get_id(ctx, &buf, sizeof(buf)); mu_Rect r = mu_layout_next(ctx); - return mu_textbox_raw(ctx, buf, bufsz, id, r, opt); + return mu_textbox_raw(ctx, buf, bufsz, 0, id, r, opt); } diff --git a/src/microui/microui.h b/src/microui/microui.h index 17142a5..e48fdf3 100644 --- a/src/microui/microui.h +++ b/src/microui/microui.h @@ -229,6 +229,11 @@ struct mu_Context { int key_down; int key_pressed; char input_text[32]; + char preedit[32]; /* IME composition text, not yet committed */ + /* Set by mu_textbox_input() while a textbox has the keyboard focus, so the + ** host can enable the IME and place its candidate window. */ + int text_input; + mu_Rect text_input_rect; }; @@ -263,6 +268,7 @@ void mu_input_scroll(mu_Context *ctx, int x, int y); void mu_input_keydown(mu_Context *ctx, int key); void mu_input_keyup(mu_Context *ctx, int key); void mu_input_text(mu_Context *ctx, const char *text); +void mu_input_preedit(mu_Context *ctx, const char *text); mu_Command* mu_push_command(mu_Context *ctx, int type, int size); int mu_next_command(mu_Context *ctx, mu_Command **cmd); @@ -298,7 +304,12 @@ void mu_text(mu_Context *ctx, const char *text); void mu_label(mu_Context *ctx, const char *text); int mu_button_ex(mu_Context *ctx, const char *label, int icon, int opt); int mu_checkbox(mu_Context *ctx, const char *label, int *state); -int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, mu_Id id, mu_Rect r, int opt); +/* Edit `buf` in place, without drawing. `max_chars` limits the length in UTF-8 +** characters (0 for no limit beyond `bufsz`). Use this with mu_draw_textbox(), +** or with custom drawing when the default appearance does not fit. */ +int mu_textbox_input(mu_Context *ctx, char *buf, int bufsz, int max_chars, mu_Id id, mu_Rect r, int opt); +void mu_draw_textbox(mu_Context *ctx, const char *buf, mu_Id id, mu_Rect r, int opt); +int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, int max_chars, mu_Id id, mu_Rect r, int opt); int mu_textbox_ex(mu_Context *ctx, char *buf, int bufsz, int opt); int mu_slider_ex(mu_Context *ctx, mu_Real *value, mu_Real low, mu_Real high, mu_Real step, const char *fmt, int opt); int mu_number_ex(mu_Context *ctx, mu_Real *value, mu_Real step, const char *fmt, int opt); diff --git a/src/modal.c b/src/modal.c index c9135ec..943435d 100644 --- a/src/modal.c +++ b/src/modal.c @@ -120,6 +120,9 @@ bool modal_default_handler(const SDL_Event *e, modal *modal) { case SDL_TEXTINPUT: mu_input_text(ctx, e->text.text); break; + case SDL_TEXTEDITING: + mu_input_preedit(ctx, e->edit.text); + break; case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: { int b = 0; @@ -161,7 +164,9 @@ bool modal_default_handler(const SDL_Event *e, modal *modal) { break; case SDL_KEYDOWN: case SDL_KEYUP: { - if (e->type == SDL_KEYDOWN && e->key.keysym.sym == SDLK_ESCAPE) + // Esc cancels an IME composition first, not the modal. + if (e->type == SDL_KEYDOWN && e->key.keysym.sym == SDLK_ESCAPE && + !*ctx->preedit) modal->cancelled = true; int c = 0; switch (e->key.keysym.sym) { @@ -293,6 +298,27 @@ static void modal_render(void) { SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE); } +// Turn SDL's text input on while a textbox has the keyboard focus, and keep the +// IME candidate window near it. The rect is in logical (view) coordinates. +static void update_text_input(void) { + static SDL_Rect last_rect; + if (!ctx->text_input) { + if (SDL_IsTextInputActive()) + SDL_StopTextInput(); + return; + } + if (!SDL_IsTextInputActive()) { + SDL_StartTextInput(); + last_rect = (SDL_Rect){0, 0, 0, 0}; + } + mu_Rect r = ctx->text_input_rect; + SDL_Rect wr = gfx_viewToWindowRect((SDL_Rect){r.x, r.y, r.w, r.h}); + if (memcmp(&wr, &last_rect, sizeof(wr))) { + last_rect = wr; + SDL_SetTextInputRect(&wr); + } +} + bool modal_handle_event(const SDL_Event *e) { if (!current_modal) return false; @@ -341,6 +367,7 @@ void modal_run(modal *m) { mu_begin(ctx); open = m->build(ctx, m); mu_end(ctx); + update_text_input(); // Present only when the overlay changed. mu_Id hash = mu_get_id(ctx, ctx->command_list.items, ctx->command_list.idx); @@ -355,6 +382,8 @@ void modal_run(modal *m) { current_modal = NULL; free(ctx); ctx = NULL; + if (SDL_IsTextInputActive()) + SDL_StopTextInput(); gfx_requestRedraw(); // repaint once more to clear the overlay // The gesture that opened/dismissed the dialog must not leave the engine