From 8c3d48c5cdb2ae4e8bdbdc48f155f53d1a92973b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Wed, 2 Dec 2020 18:52:50 +0100 Subject: [PATCH] ime: render pre-edit text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is done by allocating cells for the pre-edit text when receiving the text-input::done() call, and populating them by converting the utf-8 formatted pre-edit text to wchars. We also convert the pre-edit cursor position to cell positions (it can cover multiple cells). When rendering, we simply render the pre-edit cells on-top off the regular grid. While doing so, we also mark the underlying, “real”, cells as dirty, to ensure they are re-rendered when the pre-edit text is modified or removed. --- ime.c | 209 +++++++++++++++++++++++++++++++++++++++++++++++------ input.c | 4 +- render.c | 162 ++++++++++++++++++++++++++++++++++------- terminal.c | 9 +++ terminal.h | 13 ++++ vt.c | 2 +- wayland.c | 4 +- wayland.h | 21 ++++-- 8 files changed, 364 insertions(+), 60 deletions(-) diff --git a/ime.c b/ime.c index 9bcf0a9f..787985cf 100644 --- a/ime.c +++ b/ime.c @@ -5,9 +5,11 @@ #include "text-input-unstable-v3.h" #define LOG_MODULE "ime" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" +#include "render.h" #include "terminal.h" +#include "util.h" #include "wayland.h" #include "xmalloc.h" @@ -64,6 +66,12 @@ leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, zwp_text_input_v3_disable(seat->wl_text_input); zwp_text_input_v3_commit(seat->wl_text_input); seat->ime.serial++; + + free(seat->ime.preedit.pending.text); + seat->ime.preedit.pending.text = NULL; + + free(seat->ime.commit.pending.text); + seat->ime.commit.pending.text = NULL; } static void @@ -71,12 +79,13 @@ preedit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor_begin, int32_t cursor_end) { LOG_DBG("preedit-string: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end); + struct seat *seat = data; - free(seat->ime.preedit.text); - seat->ime.preedit.text = text != NULL ? xstrdup(text) : NULL; - seat->ime.preedit.cursor_begin = cursor_begin; - seat->ime.preedit.cursor_end = cursor_end; + free(seat->ime.preedit.pending.text); + seat->ime.preedit.pending.text = text != NULL ? xstrdup(text) : NULL; + seat->ime.preedit.pending.cursor_begin = cursor_begin; + seat->ime.preedit.pending.cursor_end = cursor_end; } static void @@ -84,10 +93,10 @@ commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, const char *text) { LOG_DBG("commit: text=%s", text); + struct seat *seat = data; - free(seat->ime.commit.text); - seat->ime.commit.text = text != NULL ? xstrdup(text) : NULL; - //term_to_slave(seat->kbd_focus, text, strlen(text)); + free(seat->ime.commit.pending.text); + seat->ime.commit.pending.text = text != NULL ? xstrdup(text) : NULL; } static void @@ -95,9 +104,10 @@ delete_surrounding_text(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t before_length, uint32_t after_length) { LOG_DBG("delete-surrounding: before=%d, after=%d", before_length, after_length); + struct seat *seat = data; - seat->ime.surrounding.before_length = before_length; - seat->ime.surrounding.after_length = after_length; + seat->ime.surrounding.pending.before_length = before_length; + seat->ime.surrounding.pending.after_length = after_length; } static void @@ -105,30 +115,185 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t serial) { /* + * From text-input-unstable-v3.h: + * * The application must proceed by evaluating the changes in the * following order: * - * 1. Replace existing preedit string with the cursor. 2. Delete - * requested surrounding text. 3. Insert commit string with the - * cursor at its end. 4. Calculate surrounding text to send. 5. - * Insert new preedit text in cursor position. 6. Place cursor - * inside preedit text. + * 1. Replace existing preedit string with the cursor. + * 2. Delete requested surrounding text. + * 3. Insert commit string with the cursor at its end. + * 4. Calculate surrounding text to send. + * 5. Insert new preedit text in cursor position. + * 6. Place cursor inside preedit text. */ LOG_DBG("done: serial=%u", serial); struct seat *seat = data; - if (seat->ime.serial != serial) - LOG_WARN("IME serial mismatch: expected=0x%08x, got 0x%08x", - seat->ime.serial, serial); + if (seat->ime.serial != serial) { + LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x", + seat->ime.serial, serial); + return; + } assert(seat->kbd_focus); + struct terminal *term = seat->kbd_focus; - if (seat->ime.commit.text != NULL) - term_to_slave(seat->kbd_focus, seat->ime.commit.text, strlen(seat->ime.commit.text)); + /* 1. Delete existing pre-edit text */ + if (term->ime.preedit.cells != NULL) { + free(term->ime.preedit.cells); + term->ime.preedit.cells = NULL; + term->ime.preedit.count = 0; + render_refresh(term); + } - free(seat->ime.commit.text); - seat->ime.commit.text = NULL; + /* + * 2. Delete requested surroundin text + * + * We don't support deleting surrounding text. But, we also never + * call set_surrounding_text() so hopefully we should never + * receive any requests to delete surrounding text. + */ + + /* 3. Insert commit string */ + if (seat->ime.commit.pending.text != NULL) { + term_to_slave( + term, + seat->ime.commit.pending.text, + strlen(seat->ime.commit.pending.text)); + + free(seat->ime.commit.pending.text); + seat->ime.commit.pending.text = NULL; + } + + /* 4. Calculate surrounding text to send - not supported */ + + /* 5. Insert new pre-edit text */ + size_t wchars = seat->ime.preedit.pending.text != NULL + ? mbstowcs(NULL, seat->ime.preedit.pending.text, 0) + : 0; + + if (wchars > 0) { + /* First, convert to unicode */ + wchar_t wcs[wchars + 1]; + mbstowcs(wcs, seat->ime.preedit.pending.text, wchars); + + /* Next, count number of cells needed */ + size_t cell_count = 0; + for (size_t i = 0; i < wchars; i++) + cell_count += max(wcwidth(wcs[i]), 1); + + /* Allocate cells */ + term->ime.preedit.cells = xmalloc( + cell_count * sizeof(term->ime.preedit.cells[0])); + term->ime.preedit.count = cell_count; + + /* Populate cells */ + for (size_t i = 0, cell_idx = 0; i < wchars; i++) { + struct cell *cell = &term->ime.preedit.cells[cell_idx]; + + int width = max(wcwidth(wcs[i]), 1); + + cell->wc = wcs[i]; + cell->attrs = (struct attributes){.clean = 0, .underline = 1}; + + for (int j = 1; j < width; j++) { + cell = &term->ime.preedit.cells[cell_idx + j]; + cell->wc = CELL_MULT_COL_SPACER; + cell->attrs = (struct attributes){.clean = 1}; + } + + cell_idx += width; + } + + /* Pre-edit cursor - hidden */ + if (seat->ime.preedit.pending.cursor_begin == -1 || + seat->ime.preedit.pending.cursor_end == -1) + { + /* Note: docs says *both* begin and end should be -1, + * but what else can we do if only one is -1? */ + LOG_DBG("pre-edit cursor is hidden"); + term->ime.preedit.cursor.hidden = true; + term->ime.preedit.cursor.start = -1; + term->ime.preedit.cursor.end = -1; + } + + else { + /* + * Translate cursor position to cell indices + * + * The cursor_begin and cursor_end are counted in + * *bytes*. We want to map them to *cell* indices. + * + * To do this, we use mblen() to step though the utf-8 + * pre-edit string, advancing a unicode character index as + * we go, *and* advancing a *cell* index using wcwidth() + * of the unicode character. + * + * When we find the matching *byte* index, we at the same + * time know both the unicode *and* cell index. + * + * Note that this has only been tested with + * + * cursor_begin == cursor_end == 0 + * + * I haven't found an IME that requests anything else + */ + + const size_t byte_len = strlen(seat->ime.preedit.pending.text); + + int cell_begin = -1, cell_end = -1; + for (size_t byte_idx = 0, wc_idx = 0, cell_idx = 0; + byte_idx < byte_len && + wc_idx < wchars && + cell_idx < cell_count && + (cell_begin < 0 || cell_end < 0); + ) + { + if (seat->ime.preedit.pending.cursor_begin == byte_idx) + cell_begin = cell_idx; + if (seat->ime.preedit.pending.cursor_end == byte_idx) + cell_end = cell_idx; + + /* Number of bytes of *next* utf-8 character */ + size_t left = byte_len - byte_idx; + int wc_bytes = mblen(&seat->ime.preedit.pending.text[byte_idx], left); + + if (wc_bytes <= 0) + break; + + byte_idx += wc_bytes; + cell_idx += max(wcwidth(term->ime.preedit.cells[wc_idx].wc), 1); + wc_idx++; + } + + /* Bounded by number of screen columns */ + cell_begin = min(max(cell_begin, 0), cell_count - 1); + + /* Ensure end comes *after* begin, and is bounded by screen */ + if (cell_end <= cell_begin) + cell_end = cell_begin + max(wcwidth(term->ime.preedit.cells[cell_begin].wc), 1); + cell_end = min(max(cell_end, 0), cell_count); + + LOG_DBG("pre-edit cursor: begin=%d, end=%d", cell_begin, cell_end); + + assert(cell_begin >= 0); + assert(cell_begin < cell_count); + assert(cell_end >= 1); + assert(cell_end <= cell_count); + assert(cell_begin < cell_end); + + term->ime.preedit.cursor.hidden = false; + term->ime.preedit.cursor.start = cell_begin; + term->ime.preedit.cursor.end = cell_end; + } + + render_refresh(term); + } + + free(seat->ime.preedit.pending.text); + seat->ime.preedit.pending.text = NULL; } const struct zwp_text_input_v3_listener text_input_listener = { diff --git a/input.c b/input.c index 4abbb89e..81129edc 100644 --- a/input.c +++ b/input.c @@ -21,7 +21,7 @@ #include #define LOG_MODULE "input" -#define LOG_ENABLE_DBG 1 +#define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "commands.h" @@ -752,7 +752,7 @@ keymap_lookup(struct seat *seat, struct terminal *term, const enum keypad_keys keypad_keys_mode = term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode; - log_dbg("keypad mode: %d, num-lock=%d", keypad_keys_mode, seat->kbd.num); + LOG_DBG("keypad mode: %d, num-lock=%d", keypad_keys_mode, seat->kbd.num); for (size_t j = 0; j < count; j++) { if (info[j].modifiers != MOD_ANY && info[j].modifiers != mods) diff --git a/render.c b/render.c index dc926cc4..88a47d47 100644 --- a/render.c +++ b/render.c @@ -313,6 +313,35 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix, cols * term->cell_width, font->strikeout.thickness}); } +static void +cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, + const pixman_color_t *fg, const pixman_color_t *bg, + pixman_color_t *cursor_color, pixman_color_t *text_color) +{ + bool is_selected = cell->attrs.selected; + + if (term->cursor_color.cursor >> 31) { + *cursor_color = color_hex_to_pixman(term->cursor_color.cursor); + *text_color = color_hex_to_pixman( + term->cursor_color.text >> 31 + ? term->cursor_color.text : term->colors.bg); + + if (term->reverse ^ cell->attrs.reverse ^ is_selected) { + pixman_color_t swap = *cursor_color; + *cursor_color = *text_color; + *text_color = swap; + } + + if (term->is_searching && !is_selected) { + color_dim_for_search(cursor_color); + color_dim_for_search(text_color); + } + } else { + *cursor_color = *fg; + *text_color = *bg; + } +} + static void draw_cursor(const struct terminal *term, const struct cell *cell, const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg, @@ -320,36 +349,14 @@ draw_cursor(const struct terminal *term, const struct cell *cell, { pixman_color_t cursor_color; pixman_color_t text_color; - - bool is_selected = cell->attrs.selected; - - if (term->cursor_color.cursor >> 31) { - cursor_color = color_hex_to_pixman(term->cursor_color.cursor); - text_color = color_hex_to_pixman( - term->cursor_color.text >> 31 - ? term->cursor_color.text : term->colors.bg); - - if (term->reverse ^ cell->attrs.reverse ^ is_selected) { - pixman_color_t swap = cursor_color; - cursor_color = text_color; - text_color = swap; - } - - if (term->is_searching && !is_selected) { - color_dim_for_search(&cursor_color); - color_dim_for_search(&text_color); - } - } else { - cursor_color = *fg; - text_color = *bg; - } + cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color); switch (term->cursor_style) { case CURSOR_BLOCK: - if (!term->kbd_focus) + if (unlikely(!term->kbd_focus || term->ime.preedit.cells != NULL)) draw_unfocused_block(term, pix, &cursor_color, x, y, cols); - else if (term->cursor_blink.state == CURSOR_BLINK_ON) { + else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) { *fg = text_color; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &cursor_color, 1, @@ -358,12 +365,17 @@ draw_cursor(const struct terminal *term, const struct cell *cell, break; case CURSOR_BAR: - if (term->cursor_blink.state == CURSOR_BLINK_ON || !term->kbd_focus) + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) + { draw_bar(term, pix, font, &cursor_color, x, y); + } break; case CURSOR_UNDERLINE: - if (term->cursor_blink.state == CURSOR_BLINK_ON || !term->kbd_focus) { + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) + { draw_underline( term, pix, attrs_to_font(term, &cell->attrs), &cursor_color, x, y, cols); @@ -1009,6 +1021,99 @@ render_sixel_images(struct terminal *term, pixman_image_t *pix) } } +static void +render_ime_preedit(struct terminal *term, struct buffer *buf, + struct coord cursor) +{ + if (likely(term->ime.preedit.cells == NULL)) + return; + + int cells_needed = term->ime.preedit.count; + + int row_idx = cursor.row; + int col_idx = cursor.col; + + int cells_left = term->cols - cursor.col; + int cells_used = min(cells_needed, term->cols); + + /* Adjust start of pre-edit text to the left if string doesn't fit on row */ + if (cells_left < cells_used) + col_idx -= cells_used - cells_left; + + assert(col_idx >= 0); + assert(col_idx < term->cols); + + struct row *row = grid_row_in_view(term->grid, row_idx); + + /* Don't start pre-edit text in the middle of a double-width character */ + while (col_idx > 0 && row->cells[col_idx].wc == CELL_MULT_COL_SPACER) { + cells_used++; + col_idx--; + } + + /* + * Copy original content (render_cell() reads cell data directly + * from grid), and mark all cells as dirty. This ensures they are + * re-rendered when the pre-edit text is modified or removed. + */ + struct cell *real_cells = malloc(cells_used * sizeof(real_cells[0])); + for (int i = 0; i < cells_used; i++) { + assert(col_idx + i < term->cols); + real_cells[i] = row->cells[col_idx + i]; + real_cells[i].attrs.clean = 0; + } + row->dirty = true; + + /* Render pre-edit text */ + for (int i = 0; i < term->ime.preedit.count; i++) { + const struct cell *cell = &term->ime.preedit.cells[i]; + + if (cell->wc == CELL_MULT_COL_SPACER) + continue; + + int width = wcwidth(term->ime.preedit.cells[i].wc); + width = max(1, width); + + if (col_idx + i + width > term->cols) + break; + + row->cells[col_idx + i] = term->ime.preedit.cells[i]; + render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false); + } + + /* Hollow cursor */ + if (!term->ime.preedit.cursor.hidden) { + int start = term->ime.preedit.cursor.start; + int end = term->ime.preedit.cursor.end; + + pixman_color_t fg = color_hex_to_pixman(term->colors.fg); + pixman_color_t bg = color_hex_to_pixman(term->colors.bg); + + pixman_color_t cursor_color, text_color; + cursor_colors_for_cell( + term, &term->ime.preedit.cells[start], + &fg, &bg, &cursor_color, &text_color); + + int x = term->margins.left + (col_idx + start) * term->cell_width; + int y = term->margins.top + row_idx * term->cell_height; + + int cols = end - start; + draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + } + + /* Restore original content (but do not render) */ + for (int i = 0; i < cells_used; i++) + row->cells[col_idx + i] = real_cells[i]; + free(real_cells); + + wl_surface_damage_buffer( + term->window->surface, + term->margins.left, + term->margins.top + row_idx * term->cell_height, + term->width - term->margins.left - term->margins.right, + 1 * term->cell_height); +} + static void render_row(struct terminal *term, pixman_image_t *pix, struct row *row, int row_no, int cursor_col) @@ -1903,6 +2008,9 @@ grid_render(struct terminal *term) term->render.workers.buf = NULL; } + /* Render IME pre-edit text */ + render_ime_preedit(term, buf, cursor); + if (term->flash.active) { /* Note: alpha is pre-computed in each color component */ /* TODO: dim while searching */ diff --git a/terminal.c b/terminal.c index 3bb41d12..d08d8658 100644 --- a/terminal.c +++ b/terminal.c @@ -1403,6 +1403,8 @@ term_destroy(struct terminal *term) tll_free(term->alt.sixel_images); sixel_fini(term); + free(term->ime.preedit.cells); + free(term->foot_exe); free(term->cwd); @@ -2216,6 +2218,13 @@ term_kbd_focus_out(struct terminal *term) if (it->item.kbd_focus == term) return; + if (term->ime.preedit.cells != NULL) { + free(term->ime.preedit.cells); + term->ime.preedit.cells = NULL; + term->ime.preedit.count = 0; + render_refresh(term); + } + term->kbd_focus = false; cursor_refresh(term); diff --git a/terminal.h b/terminal.h index 4eca0aa9..b8339d8d 100644 --- a/terminal.h +++ b/terminal.h @@ -470,6 +470,19 @@ struct terminal { unsigned max_height; /* Maximum image height, in pixels */ } sixel; + struct { + struct { + struct cell *cells; + int count; + + struct { + bool hidden; + int start; /* Cell index, inclusive */ + int end; /* Cell index, exclusive */ + } cursor; + } preedit; + } ime; + bool quit; bool is_shutting_down; void (*shutdown_cb)(void *data, int exit_code); diff --git a/vt.c b/vt.c index 1522dacf..ed26da73 100644 --- a/vt.c +++ b/vt.c @@ -408,7 +408,7 @@ action_esc_dispatch(struct terminal *term, uint8_t final) case '=': term->keypad_keys_mode = KEYPAD_APPLICATION; break; -ö + case '>': term->keypad_keys_mode = KEYPAD_NUMERICAL; break; diff --git a/wayland.c b/wayland.c index ecaecb1c..c0a9c512 100644 --- a/wayland.c +++ b/wayland.c @@ -191,8 +191,8 @@ seat_destroy(struct seat *seat) free(seat->clipboard.text); free(seat->primary.text); - free(seat->ime.preedit.text); - free(seat->ime.commit.text); + free(seat->ime.preedit.pending.text); + free(seat->ime.commit.pending.text); free(seat->name); } diff --git a/wayland.h b/wayland.h index b29bb8ab..51a6fad1 100644 --- a/wayland.h +++ b/wayland.h @@ -15,6 +15,9 @@ #include "fdm.h" +/* Forward declarations */ +struct terminal; + typedef tll(xkb_keycode_t) xkb_keycode_list_t; struct key_binding { @@ -225,18 +228,24 @@ struct seat { struct zwp_text_input_v3 *wl_text_input; struct { struct { - char *text; - int32_t cursor_begin; - int32_t cursor_end; + struct { + char *text; + int32_t cursor_begin; + int32_t cursor_end; + } pending; } preedit; struct { - char *text; + struct { + char *text; + } pending; } commit; struct { - uint32_t before_length; - uint32_t after_length; + struct { + uint32_t before_length; + uint32_t after_length; + } pending; } surrounding; uint32_t serial;