diff --git a/CHANGELOG.md b/CHANGELOG.md index ac69ddd3..c8a2877f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,12 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Added +* IME support. This is compile-time optional, see + [INSTALL.md](INSTALL.md#user-content-options) + (https://codeberg.org/dnkl/foot/issues/134). +* `DECSET` escape to enable/disable IME: `\E[?737769h` enables IME and + `\E[?737769l` disables IME. This can be used to e.g. enable/disable + IME when entering/leaving insert mode in vim. * Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and `CSI ? 45 l`. It is **enabled** by default diff --git a/INSTALL.md b/INSTALL.md index 40e167a2..43655b81 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -7,6 +7,7 @@ 1. [Arch Linux](#arch-linux) 1. [Other](#other) 1. [Setup](#setup) + 1. [Options](#options) 1. [Release build](#release-build) 1. [Size optimized](#size-optimized) 1. [Performance optimized, non-PGO](#performance-optimized-non-pgo) @@ -130,6 +131,15 @@ To build, first, create a build directory, and switch to it: mkdir -p bld/release && cd bld/release ``` +### Options + +Available compile-time options: + +| Option | Type | Default | Description | Extra dependencies | +|----------|------|---------|---------------------|--------------------| +| `-Dime` | bool | `true` | Enables IME support | None | + + ### Release build Below are instructions for building foot either [size diff --git a/client.c b/client.c index 9887c514..58a500eb 100644 --- a/client.c +++ b/client.c @@ -16,6 +16,7 @@ #define LOG_ENABLE_DBG 0 #include "log.h" #include "client-protocol.h" +#include "foot-features.h" #include "version.h" #include "xmalloc.h" @@ -27,6 +28,15 @@ sig_handler(int signo) aborted = 1; } +static const char * +version_and_features(void) +{ + static char buf[256]; + snprintf(buf, sizeof(buf), "version: %s %cime", + FOOT_VERSION, feature_ime() ? '+' : '-'); + return buf; +} + static void print_usage(const char *prog_name) { @@ -155,7 +165,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("footclient version %s\n", FOOT_VERSION); + printf("footclient %s\n", version_and_features()); return EXIT_SUCCESS; case 'h': diff --git a/csi.c b/csi.c index f70c58b2..232f7b1d 100644 --- a/csi.c +++ b/csi.c @@ -540,6 +540,13 @@ decset_decrst(struct terminal *term, unsigned param, bool enable) term->modify_escape_key = enable; break; + case 737769: + if (enable) + term_ime_enable(term); + else + term_ime_disable(term); + break; + default: UNHANDLED(); break; @@ -588,6 +595,7 @@ xtsave(struct terminal *term, unsigned param) case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break; case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; case 27127: term->xtsave.modify_escape_key = term->modify_escape_key; break; + case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; } } @@ -622,6 +630,7 @@ xtrestore(struct terminal *term, unsigned param) case 1049: enable = term->xtsave.alt_screen; break; case 2004: enable = term->xtsave.bracketed_paste; break; case 27127: enable = term->xtsave.modify_escape_key; break; + case 737769: enable = term->xtsave.ime; break; default: return; } diff --git a/foot-features.h b/foot-features.h new file mode 100644 index 00000000..8da22f6c --- /dev/null +++ b/foot-features.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +static inline bool feature_ime(void) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + return true; +#else + return false; +#endif +} diff --git a/ime.c b/ime.c new file mode 100644 index 00000000..24237d69 --- /dev/null +++ b/ime.c @@ -0,0 +1,376 @@ +#include "ime.h" + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + +#include + +#include "text-input-unstable-v3.h" + +#define LOG_MODULE "ime" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "render.h" +#include "search.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xmalloc.h" + +static void +enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface) +{ + struct seat *seat = data; + + LOG_DBG("enter: seat=%s", seat->name); + + /* The main grid is the *only* input-receiving surface we have */ + /* TODO: can we receive text_input::enter() _before_ keyboard_enter()? */ + struct terminal UNUSED *term = seat->kbd_focus; + assert(term != NULL); + assert(term_surface_kind(term, surface) == TERM_SURF_GRID); + + ime_enable(seat); +} + +static void +leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface) +{ + struct seat *seat = data; + LOG_DBG("leave: seat=%s", seat->name); + ime_disable(seat); +} + +static void +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; + + ime_reset_preedit(seat); + + if (text != NULL) { + seat->ime.preedit.pending.text = xstrdup(text); + seat->ime.preedit.pending.cursor_begin = cursor_begin; + seat->ime.preedit.pending.cursor_end = cursor_end; + } +} + +static void +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; + + ime_reset_commit(seat); + + if (text != NULL) + seat->ime.commit.pending.text = xstrdup(text); +} + +static void +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.pending.before_length = before_length; + seat->ime.surrounding.pending.after_length = after_length; +} + +static void +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. + */ + + LOG_DBG("done: serial=%u", serial); + struct seat *seat = data; + + 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; + + /* 1. Delete existing pre-edit text */ + if (term->ime.preedit.cells != NULL) { + term_ime_reset(term); + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); + } + + /* + * 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) { + const char *text = seat->ime.commit.pending.text; + size_t len = strlen(text); + + if (term->is_searching) { + search_add_chars(term, text, len); + render_refresh_search(term); + } else + term_to_slave(term, text, len); + ime_reset_commit(seat); + } + + /* 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 || wchars == (size_t)-1) { + ime_reset_preedit(seat); + return; + } + + /* First, convert to unicode */ + term->ime.preedit.text = xmalloc((wchars + 1) * sizeof(wchar_t)); + mbstowcs(term->ime.preedit.text, seat->ime.preedit.pending.text, wchars); + term->ime.preedit.text[wchars] = L'\0'; + + /* Next, count number of cells needed */ + size_t cell_count = 0; + size_t widths[wchars + 1]; + + for (size_t i = 0; i < wchars; i++) { + int width = max(wcwidth(term->ime.preedit.text[i]), 1); + widths[i] = width; + cell_count += width; + } + + /* 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 = widths[i]; + + cell->wc = term->ime.preedit.text[i]; + cell->attrs = (struct attributes){.clean = 0}; + + 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); + cell_idx += widths[wc_idx], wc_idx++) + { + 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; + } + + if (seat->ime.preedit.pending.cursor_end >= byte_len) + cell_end = cell_count; + + /* Bounded by number of screen columns */ + cell_begin = min(max(cell_begin, 0), cell_count - 1); + cell_end = min(max(cell_end, 0), cell_count); + + if (cell_end < cell_begin) + cell_end = cell_begin; + + /* Expand cursor end to end of glyph */ + while (cell_end > cell_begin && cell_end < cell_count && + term->ime.preedit.cells[cell_end].wc == CELL_MULT_COL_SPACER) + { + cell_end++; + } + + LOG_DBG("pre-edit cursor: begin=%d, end=%d", cell_begin, cell_end); + + assert(cell_begin >= 0); + assert(cell_begin < cell_count); + assert(cell_begin <= cell_end); + assert(cell_end >= 0); + assert(cell_end <= cell_count); + + term->ime.preedit.cursor.hidden = false; + term->ime.preedit.cursor.start = cell_begin; + term->ime.preedit.cursor.end = cell_end; + } + + /* Underline pre-edit string that is *not* covered by the cursor */ + bool hidden = term->ime.preedit.cursor.hidden; + int start = term->ime.preedit.cursor.start; + int end = term->ime.preedit.cursor.end; + + for (size_t i = 0, cell_idx = 0; i < wchars; cell_idx += widths[i], i++) { + if (hidden || start == end || cell_idx < start || cell_idx >= end) { + struct cell *cell = &term->ime.preedit.cells[cell_idx]; + cell->attrs.underline = true; + } + } + + ime_reset_preedit(seat); + + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); +} + +void +ime_reset_preedit(struct seat *seat) +{ + free(seat->ime.preedit.pending.text); + seat->ime.preedit.pending.text = NULL; +} + +void +ime_reset_commit(struct seat *seat) +{ + free(seat->ime.commit.pending.text); + seat->ime.commit.pending.text = NULL; +} + +void +ime_reset(struct seat *seat) +{ + ime_reset_preedit(seat); + ime_reset_commit(seat); +} + +void +ime_enable(struct seat *seat) +{ + struct terminal *term = seat->kbd_focus; + assert(term != NULL); + + if (!term->ime.enabled) + return; + + ime_reset(seat); + + zwp_text_input_v3_enable(seat->wl_text_input); + zwp_text_input_v3_set_content_type( + seat->wl_text_input, + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL); + + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; +} + +void +ime_disable(struct seat *seat) +{ + ime_reset(seat); + + zwp_text_input_v3_disable(seat->wl_text_input); + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; +} + +const struct zwp_text_input_v3_listener text_input_listener = { + .enter = &enter, + .leave = &leave, + .preedit_string = &preedit_string, + .commit_string = &commit_string, + .delete_surrounding_text = &delete_surrounding_text, + .done = &done, +}; + +#else /* !FOOT_IME_ENABLED */ + +void ime_enable(struct seat *seat) {} +void ime_disable(struct seat *seat) {} + +void ime_reset_preedit(struct seat *seat) {} +void ime_reset_commit(struct seat *seat) {} +void ime_reset(struct seat *seat) {} + +#endif diff --git a/ime.h b/ime.h new file mode 100644 index 00000000..7036bbde --- /dev/null +++ b/ime.h @@ -0,0 +1,18 @@ +#pragma once + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + +#include "text-input-unstable-v3.h" + +extern const struct zwp_text_input_v3_listener text_input_listener; + +#endif /* FOOT_IME_ENABLED */ + +struct seat; + +void ime_enable(struct seat *seat); +void ime_disable(struct seat *seat); + +void ime_reset_preedit(struct seat *seat); +void ime_reset_commit(struct seat *seat); +void ime_reset(struct seat *seat); diff --git a/input.c b/input.c index 31c1d816..81129edc 100644 --- a/input.c +++ b/input.c @@ -752,6 +752,8 @@ 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); + for (size_t j = 0; j < count; j++) { if (info[j].modifiers != MOD_ANY && info[j].modifiers != mods) continue; diff --git a/main.c b/main.c index ab595d22..59ac9b61 100644 --- a/main.c +++ b/main.c @@ -21,6 +21,7 @@ #include "log.h" #include "config.h" +#include "foot-features.h" #include "fdm.h" #include "reaper.h" #include "render.h" @@ -39,6 +40,15 @@ sig_handler(int signo) aborted = 1; } +static const char * +version_and_features(void) +{ + static char buf[256]; + snprintf(buf, sizeof(buf), "version: %s %cime", + FOOT_VERSION, feature_ime() ? '+' : '-'); + return buf; +} + static void print_usage(const char *prog_name) { @@ -325,7 +335,7 @@ main(int argc, char *const *argv) break; case 'v': - printf("foot version %s\n", FOOT_VERSION); + printf("foot %s\n", version_and_features()); return EXIT_SUCCESS; case 'h': @@ -343,7 +353,7 @@ main(int argc, char *const *argv) argc -= optind; argv += optind; - LOG_INFO("version: %s", FOOT_VERSION); + LOG_INFO("%s", version_and_features()); { struct utsname name; diff --git a/meson.build b/meson.build index 6b9a8c4d..36c5807e 100644 --- a/meson.build +++ b/meson.build @@ -17,6 +17,9 @@ add_project_arguments( (is_debug_build ? ['-D_DEBUG'] : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) + + (get_option('ime') + ? ['-DFOOT_IME_ENABLED=1'] + : []) + cc.get_supported_arguments( ['-pedantic', '-fstrict-aliasing', @@ -78,6 +81,7 @@ foreach prot : [ wayland_protocols_datadir + '/unstable/xdg-output/xdg-output-unstable-v1.xml', wayland_protocols_datadir + '/unstable/primary-selection/primary-selection-unstable-v1.xml', wayland_protocols_datadir + '/stable/presentation-time/presentation-time.xml', + wayland_protocols_datadir + '/unstable/text-input/text-input-unstable-v3.xml', ] wl_proto_headers += custom_target( @@ -147,6 +151,8 @@ executable( 'commands.c', 'commands.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', + 'foot-features.h', + 'ime.c', 'ime.h', 'input.c', 'input.h', 'main.c', 'quirks.c', 'quirks.h', @@ -169,6 +175,7 @@ executable( executable( 'footclient', 'client.c', 'client-protocol.h', + 'foot-features.h', 'log.c', 'log.h', 'macros.h', 'xmalloc.c', 'xmalloc.h', @@ -196,9 +203,9 @@ subdir('completions') subdir('doc') subdir('icons') -# summary( -# { -# '': false, -# }, -# bool_yn: true -# ) +summary( + { + 'IME': get_option('ime'), + }, + bool_yn: true +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 00000000..fc3c1b0f --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('ime', type: 'boolean', value: true, description: 'IME (Input Method Editor) support') diff --git a/pgo/pgo.c b/pgo/pgo.c index d401e533..0dc08f61 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -126,6 +126,9 @@ extract_finish(struct extraction_context *context, char **text, size_t *len) void cmd_scrollback_up(struct terminal *term, int rows) {} void cmd_scrollback_down(struct terminal *term, int rows) {} +void ime_enable(struct seat *seat) {} +void ime_disable(struct seat *seat) {} + int main(int argc, const char *const *argv) { diff --git a/render.c b/render.c index dc926cc4..df22dd6e 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)) 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,151 @@ render_sixel_images(struct terminal *term, pixman_image_t *pix) } } +static void +render_ime_preedit(struct terminal *term, struct buffer *buf) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + + if (likely(term->ime.preedit.cells == NULL)) + return; + + if (unlikely(term->is_searching)) + return; + + /* Adjust cursor position to viewport */ + struct coord cursor; + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + + if (cursor.row < 0 || cursor.row >= term->rows) + return; + + int cells_needed = term->ime.preedit.count; + + int row_idx = cursor.row; + int col_idx = cursor.col; + int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */ + + 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; + + if (cells_needed > cells_used) { + int start = term->ime.preedit.cursor.start; + int end = term->ime.preedit.cursor.end; + + if (start == end) { + /* Ensure *end* of pre-edit string is visible */ + ime_ofs = cells_needed - cells_used; + } else { + /* Ensure the *beginning* of the cursor-area is visible */ + ime_ofs = start; + + /* Display as much as possible of the pre-edit string */ + if (cells_needed - ime_ofs < cells_used) + ime_ofs = cells_needed - cells_used; + } + + /* Make sure we don't start in the middle of a character */ + while (ime_ofs < cells_needed && + term->ime.preedit.cells[ime_ofs].wc == CELL_MULT_COL_SPACER) + { + ime_ofs++; + } + } + + 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 */ + assert(term->ime.preedit.cells[ime_ofs].wc != CELL_MULT_COL_SPACER); + for (int i = 0, idx = ime_ofs; idx < term->ime.preedit.count; i++, idx++) { + const struct cell *cell = &term->ime.preedit.cells[idx]; + + if (cell->wc == CELL_MULT_COL_SPACER) + continue; + + int width = max(1, wcwidth(cell->wc)); + if (col_idx + i + width > term->cols) + break; + + row->cells[col_idx + i] = *cell; + render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false); + } + + int start = term->ime.preedit.cursor.start - ime_ofs; + int end = term->ime.preedit.cursor.end - ime_ofs; + + if (!term->ime.preedit.cursor.hidden) { + const struct cell *start_cell = &term->ime.preedit.cells[start + ime_ofs]; + + 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, start_cell, &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; + + if (end == start) { + /* Bar */ + if (start >= 0) { + struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); + draw_bar(term, buf->pix[0], font, &cursor_color, x, y); + } + } + + else if (end > start) { + /* Hollow cursor */ + if (start >= 0 && end <= term->cols) { + 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); +#endif +} + static void render_row(struct terminal *term, pixman_image_t *pix, struct row *row, int row_no, int cursor_col) @@ -1903,6 +2060,9 @@ grid_render(struct terminal *term) term->render.workers.buf = NULL; } + /* Render IME pre-edit text */ + render_ime_preedit(term, buf); + if (term->flash.active) { /* Note: alpha is pre-computed in each color component */ /* TODO: dim while searching */ @@ -1985,7 +2145,49 @@ render_search_box(struct terminal *term) { assert(term->window->search_sub_surface != NULL); - const size_t wanted_visible_chars = max(20, term->search.len); + /* + * We treat the search box pretty much like a row of cells. That + * is, a glyph is either 1 or 2 (or more) “cells” wide. + * + * The search ‘length’, and ‘cursor’ (position) is in + * *characters*, not cells. This means we need to translate from + * character count to cell count when calculating the length of + * the search box, where in the search string we should start + * rendering etc. + */ + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + size_t text_len = term->search.len; + if (term->ime.preedit.text != NULL) + text_len += wcslen(term->ime.preedit.text); + + wchar_t *text = xmalloc((text_len + 1) * sizeof(wchar_t)); + text[0] = L'\0'; + + /* Copy everything up to the cursor */ + wcsncpy(text, term->search.buf, term->search.cursor); + text[term->search.cursor] = L'\0'; + + /* Insert pre-edit text at cursor */ + if (term->ime.preedit.text != NULL) + wcscat(text, term->ime.preedit.text); + + /* And finally everything after the cursor */ + wcsncat(text, &term->search.buf[term->search.cursor], + term->search.len - term->search.cursor); +#else + const wchar_t *text = term->search.buf; + const size_t text_len = term->search.len; +#endif + + /* Calculate the width of each character */ + int widths[text_len + 1]; + for (size_t i = 0; i < text_len; i++) + widths[i] = max(1, wcwidth(text[i])); + widths[text_len] = 0; + + const size_t total_cells = wcswidth(text, text_len); + const size_t wanted_visible_cells = max(20, total_cells); assert(term->scale >= 1); const int scale = term->scale; @@ -1995,12 +2197,12 @@ render_search_box(struct terminal *term) const size_t width = term->width - 2 * margin; const size_t visible_width = min( term->width - 2 * margin, - 2 * margin + wanted_visible_chars * term->cell_width); + 2 * margin + wanted_visible_cells * term->cell_width); const size_t height = min( term->height - 2 * margin, 2 * margin + 1 * term->cell_height); - const size_t visible_chars = (visible_width - 2 * margin) / term->cell_width; + const size_t visible_cells = (visible_width - 2 * margin) / term->cell_width; size_t glyph_offset = term->render.search_glyph_offset; unsigned long cookie = shm_cookie_search(term); @@ -2008,7 +2210,7 @@ render_search_box(struct terminal *term) /* Background - yellow on empty/match, red on mismatch */ pixman_color_t color = color_hex_to_pixman( - term->search.match_len == term->search.len + term->search.match_len == text_len ? term->colors.table[3] : term->colors.table[1]); pixman_image_fill_rectangles( @@ -2021,49 +2223,176 @@ render_search_box(struct terminal *term) 1, &(pixman_rectangle16_t){0, 0, width - visible_width, height}); struct fcft_font *font = term->fonts[0]; - int x = width - visible_width + margin; + const int x_left = width - visible_width + margin; + int x = x_left; int y = margin; pixman_color_t fg = color_hex_to_pixman(term->colors.table[0]); - /* Ensure cursor is visible */ - if (term->search.cursor < glyph_offset) - term->render.search_glyph_offset = glyph_offset = term->search.cursor; - else if (term->search.cursor > glyph_offset + visible_chars) { - term->render.search_glyph_offset = glyph_offset = - term->search.cursor - min(term->search.cursor, visible_chars); - } - - /* Move offset if there is free space available */ - if (term->search.len - glyph_offset < visible_chars) - term->render.search_glyph_offset = glyph_offset = - term->search.len - min(term->search.len, visible_chars); - - /* Text (what the user entered - *not* match(es)) */ - for (size_t i = glyph_offset; - i < term->search.len && i - glyph_offset < visible_chars; - i++) - { - if (i == term->search.cursor) - draw_bar(term, buf->pix[0], font, &fg, x, y); - - const struct fcft_glyph *glyph = fcft_glyph_rasterize( - font, term->search.buf[i], term->font_subpixel); - - if (glyph == NULL) + /* Move offset we start rendering at, to ensure the cursor is visible */ + for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { + if (i != term->search.cursor) continue; - pixman_image_t *src = pixman_image_create_solid_fill(&fg); - pixman_image_composite32( - PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, - x + glyph->x, y + font_baseline(term) - glyph->y, - glyph->width, glyph->height); - pixman_image_unref(src); +#if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime.preedit.cells != NULL) { + if (term->ime.preedit.cursor.start == term->ime.preedit.cursor.end) { + /* All IME's I've seen so far keeps the cursor at + * index 0, so ensure the *end* of the pre-edit string + * is visible */ + cell_idx += term->ime.preedit.count; + } else { + /* Try to predict in which direction we'll shift the text */ + if (cell_idx + term->ime.preedit.cursor.start > glyph_offset) + cell_idx += term->ime.preedit.cursor.end; + else + cell_idx += term->ime.preedit.cursor.start; + } + } +#endif - x += term->cell_width; + if (cell_idx < glyph_offset) { + /* Shift to the *left*, making *this* character the + * *first* visible one */ + term->render.search_glyph_offset = glyph_offset = cell_idx; + } + + else if (cell_idx > glyph_offset + visible_cells) { + /* Shift to the *right*, making *this* character the + * *last* visible one */ + term->render.search_glyph_offset = glyph_offset = + cell_idx - min(cell_idx, visible_cells); + } + + /* Adjust offset if there is free space available */ + if (total_cells - glyph_offset < visible_cells) { + term->render.search_glyph_offset = glyph_offset = + total_cells - min(total_cells, visible_cells); + } + + break; } - if (term->search.cursor >= term->search.len) - draw_bar(term, buf->pix[0], font, &fg, x, y); + /* Ensure offset is at a character boundary */ + for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) { + if (cell_idx >= glyph_offset) { + term->render.search_glyph_offset = glyph_offset = cell_idx; + break; + } + } + + /* + * Render the search string, starting at ‘glyph_offset’. Note that + * glyph_offset is in cells, not characters + */ + for (size_t i = 0, + cell_idx = 0, + width = widths[i], + next_cell_idx = width; + i < text_len; + i++, + cell_idx = next_cell_idx, + width = widths[i], + next_cell_idx += width) + { + /* Render cursor */ + if (i == term->search.cursor) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + bool have_preedit = term->ime.preedit.cells != NULL; + bool hidden = term->ime.preedit.cursor.hidden; + + if (have_preedit && !hidden) { + /* Cursor may be outside the visible area: + * cell_idx-glyph_offset can be negative */ + int cells_left = visible_cells - max( + (ssize_t)(cell_idx - glyph_offset), 0); + + /* If cursor is outside the visible area, we need to + * adjust our rectangle's position */ + int start = term->ime.preedit.cursor.start + + min((ssize_t)(cell_idx - glyph_offset), 0); + int end = term->ime.preedit.cursor.end + + min((ssize_t)(cell_idx - glyph_offset), 0); + + if (start == end) { + int count = min(term->ime.preedit.count, cells_left); + + /* Underline the entire (visible part of) pre-edit text */ + draw_underline(term, buf->pix[0], font, &fg, x, y, count); + + /* Bar-styled cursor, if in the visible area */ + if (start >= 0 && start <= visible_cells) + draw_bar(term, buf->pix[0], font, &fg, x + start * term->cell_width, y); + } else { + /* Underline everything before and after the cursor */ + int count1 = min(start, cells_left); + int count2 = max( + min(term->ime.preedit.count - term->ime.preedit.cursor.end, + cells_left - end), + 0); + draw_underline(term, buf->pix[0], font, &fg, x, y, count1); + draw_underline(term, buf->pix[0], font, &fg, x + end * term->cell_width, y, count2); + + /* TODO: how do we handle a partially hidden rectangle? */ + if (start >= 0 && end <= visible_cells) { + draw_unfocused_block( + term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); + } + } + } else if (!have_preedit) +#endif + { + /* Cursor *should* be in the visible area */ + assert(cell_idx >= glyph_offset); + assert(cell_idx <= glyph_offset + visible_cells); + draw_bar(term, buf->pix[0], font, &fg, x, y); + } + } + + if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) { + /* We're now beyond the visible area - nothing more to render */ + break; + } + + if (cell_idx < glyph_offset) { + /* We haven't yet reached the visible part of the string */ + cell_idx = next_cell_idx; + continue; + } + + const struct fcft_glyph *glyph = fcft_glyph_rasterize( + font, text[i], term->font_subpixel); + + if (glyph == NULL) { + cell_idx = next_cell_idx; + continue; + } + + if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { + /* Glyph surface is a pre-rendered image (typically a color emoji...) */ + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, + x + glyph->x, y + font_baseline(term) - glyph->y, + glyph->width, glyph->height); + } else { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, + x + glyph->x, y + font_baseline(term) - glyph->y, + glyph->width, glyph->height); + pixman_image_unref(src); + } + + x += width * term->cell_width; + cell_idx = next_cell_idx; + } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime.preedit.cells != NULL) + /* Already rendered */; + else +#endif + if (term->search.cursor >= term->search.len) + draw_bar(term, buf->pix[0], font, &fg, x, y); quirk_weston_subsurface_desync_on(term->window->search_sub_surface); @@ -2086,6 +2415,10 @@ render_search_box(struct terminal *term) wl_surface_commit(term->window->search_surface); quirk_weston_subsurface_desync_off(term->window->search_sub_surface); + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + free(text); +#endif } static void @@ -2458,32 +2791,28 @@ fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) tll_foreach(renderer->wayl->terms, it) { struct terminal *term = it->item; - if (!term->render.refresh.grid && - !term->render.refresh.csd && - !term->render.refresh.search) - { + if (unlikely(!term->window->is_configured)) continue; - } - - if (term->render.app_sync_updates.enabled && - !term->render.refresh.csd && - !term->render.refresh.search) - { - continue; - } - - if (term->render.refresh.csd || term->render.refresh.search) { - /* Force update of parent surface */ - term->render.refresh.grid = true; - } - - assert(term->window->is_configured); bool grid = term->render.refresh.grid; bool csd = term->render.refresh.csd; bool search = term->render.refresh.search; bool title = term->render.refresh.title; + if (!term->is_searching) + search = false; + + if (!(grid | csd | search | title)) + continue; + + if (term->render.app_sync_updates.enabled && !(csd | search | title)) + continue; + + if (csd | search) { + /* Force update of parent surface */ + grid = true; + } + term->render.refresh.grid = false; term->render.refresh.csd = false; term->render.refresh.search = false; diff --git a/search.c b/search.c index bd5effce..a7b6f112 100644 --- a/search.c +++ b/search.c @@ -18,6 +18,7 @@ #include "selection.h" #include "shm.h" #include "util.h" +#include "xmalloc.h" /* * Ensures a "new" viewport doesn't contain any unallocated rows. @@ -98,6 +99,12 @@ search_cancel_keep_selection(struct terminal *term) term->is_searching = false; term->render.search_glyph_offset = 0; + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } + term_xcursor_update(term); render_refresh(term); } @@ -110,6 +117,12 @@ search_begin(struct terminal *term) search_cancel_keep_selection(term); selection_cancel(term); + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } + /* On-demand instantiate wayland surface */ struct wl_window *win = term->window; struct wayland *wayl = term->wl; @@ -124,6 +137,11 @@ search_begin(struct terminal *term) term->search.view_followed_offset = term->grid->view == term->grid->offset; term->is_searching = true; + term->search.len = 0; + term->search.sz = 64; + term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0])); + term->search.buf[0] = L'\0'; + term_xcursor_update(term); render_refresh_search(term); } @@ -332,8 +350,8 @@ search_find_next(struct terminal *term) #undef ROW_DEC } -static void -add_chars(struct terminal *term, const char *src, size_t count) +void +search_add_chars(struct terminal *term, const char *src, size_t count) { mbstate_t ps = {0}; size_t wchars = mbsnrtowcs(NULL, &src, count, 0, &ps); @@ -494,7 +512,7 @@ static void from_clipboard_cb(char *text, size_t size, void *user) { struct terminal *term = user; - add_chars(term, text, size); + search_add_chars(term, text, size); } static void @@ -722,7 +740,7 @@ search_input(struct seat *seat, struct terminal *term, uint32_t key, if (count == 0) return; - add_chars(term, (const char *)buf, count); + search_add_chars(term, (const char *)buf, count); update_search: LOG_DBG("search: buffer: %ls", term->search.buf); diff --git a/search.h b/search.h index 178b6140..ece0d68a 100644 --- a/search.h +++ b/search.h @@ -7,3 +7,4 @@ void search_begin(struct terminal *term); void search_cancel(struct terminal *term); void search_input(struct seat *seat, struct terminal *term, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, uint32_t serial); +void search_add_chars(struct terminal *term, const char *text, size_t len); diff --git a/shm.c b/shm.c index 751e265d..1d08d56c 100644 --- a/shm.c +++ b/shm.c @@ -655,7 +655,7 @@ shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows, : shm_scroll_reverse(shm, buf, -rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows); } - void +void shm_purge(struct wl_shm *shm, unsigned long cookie) { LOG_DBG("cookie=%lx: purging all buffers", cookie); diff --git a/terminal.c b/terminal.c index 3bb41d12..a626b88d 100644 --- a/terminal.c +++ b/terminal.c @@ -24,6 +24,7 @@ #include "config.h" #include "extract.h" #include "grid.h" +#include "ime.h" #include "quirks.h" #include "reaper.h" #include "render.h" @@ -1116,6 +1117,11 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .shutdown_data = shutdown_data, .foot_exe = xstrdup(foot_exe), .cwd = xstrdup(cwd), +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + .ime = { + .enabled = true, + }, +#endif }; for (size_t i = 0; i < 4; i++) { @@ -1403,6 +1409,8 @@ term_destroy(struct terminal *term) tll_free(term->alt.sixel_images); sixel_fini(term); + term_ime_reset(term); + free(term->foot_exe); free(term->cwd); @@ -1557,6 +1565,10 @@ term_reset(struct terminal *term, bool hard) sixel_destroy(&it->item); tll_free(term->alt.sixel_images); +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + term_ime_enable(term); +#endif + if (!hard) return; @@ -2216,6 +2228,13 @@ term_kbd_focus_out(struct terminal *term) if (it->item.kbd_focus == term) return; +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime.preedit.cells != NULL) { + term_ime_reset(term); + render_refresh(term); + } +#endif + term->kbd_focus = false; cursor_refresh(term); @@ -2744,3 +2763,67 @@ term_view_to_text(const struct terminal *term, char **text, size_t *len) int end = grid_row_absolute_in_view(term->grid, term->rows - 1); return rows_to_text(term, start, end, text, len); } + +bool +term_ime_is_enabled(const struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + return term->ime.enabled; +#else + return false; +#endif +} + +void +term_ime_enable(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime.enabled) + return; + + LOG_DBG("IME enabled"); + + term->ime.enabled = true; + term_ime_reset(term); + + /* IME is per seat - enable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_enable(&it->item); + } +#endif +} + +void +term_ime_disable(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (!term->ime.enabled) + return; + + LOG_DBG("IME disabled"); + + term->ime.enabled = false; + term_ime_reset(term); + + /* IME is per seat - disable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_disable(&it->item); + } +#endif +} + +void +term_ime_reset(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime.preedit.cells != NULL) { + free(term->ime.preedit.text); + free(term->ime.preedit.cells); + term->ime.preedit.text = NULL; + term->ime.preedit.cells = NULL; + term->ime.preedit.count = 0; + } +#endif +} diff --git a/terminal.h b/terminal.h index 4eca0aa9..07723e1e 100644 --- a/terminal.h +++ b/terminal.h @@ -298,6 +298,7 @@ struct terminal { uint32_t bell_is_urgent:1; uint32_t alt_screen:1; uint32_t modify_escape_key:1; + uint32_t ime:1; } xtsave; char *window_title; @@ -470,6 +471,23 @@ struct terminal { unsigned max_height; /* Maximum image height, in pixels */ } sixel; +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + struct { + bool enabled; + struct { + wchar_t *text; + struct cell *cells; + int count; + + struct { + bool hidden; + int start; /* Cell index, inclusive */ + int end; /* Cell index, exclusive */ + } cursor; + } preedit; + } ime; +#endif + bool quit; bool is_shutting_down; void (*shutdown_cb)(void *data, int exit_code); @@ -592,3 +610,8 @@ bool term_scrollback_to_text( const struct terminal *term, char **text, size_t *len); bool term_view_to_text( const struct terminal *term, char **text, size_t *len); + +bool term_ime_is_enabled(const struct terminal *term); +void term_ime_enable(struct terminal *term); +void term_ime_disable(struct terminal *term); +void term_ime_reset(struct terminal *term); diff --git a/wayland.c b/wayland.c index 8007eeba..c8cb2c9c 100644 --- a/wayland.c +++ b/wayland.c @@ -18,6 +18,7 @@ #include #include #include +#include #define LOG_MODULE "wayland" #define LOG_ENABLE_DBG 0 @@ -25,6 +26,7 @@ #include "config.h" #include "terminal.h" +#include "ime.h" #include "input.h" #include "render.h" #include "selection.h" @@ -111,6 +113,25 @@ seat_add_primary_selection(struct seat *seat) primary_selection_device, &primary_selection_device_listener, seat); } +static void +seat_add_text_input(struct seat *seat) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (seat->wayl->text_input_manager == NULL) + return; + + struct zwp_text_input_v3 *text_input + = zwp_text_input_manager_v3_get_text_input( + seat->wayl->text_input_manager, seat->wl_seat); + + if (text_input == NULL) + return; + + seat->wl_text_input = text_input; + zwp_text_input_v3_add_listener(text_input, &text_input_listener, seat); +#endif +} + static void seat_destroy(struct seat *seat) { @@ -165,9 +186,16 @@ seat_destroy(struct seat *seat) wl_keyboard_release(seat->wl_keyboard); if (seat->wl_pointer != NULL) wl_pointer_release(seat->wl_pointer); + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (seat->wl_text_input != NULL) + zwp_text_input_v3_destroy(seat->wl_text_input); +#endif + if (seat->wl_seat != NULL) wl_seat_release(seat->wl_seat); + ime_reset(seat); free(seat->clipboard.text); free(seat->primary.text); free(seat->name); @@ -815,6 +843,7 @@ handle_global(void *data, struct wl_registry *registry, seat_add_data_device(seat); seat_add_primary_selection(seat); + seat_add_text_input(seat); wl_seat_add_listener(wl_seat, &seat_listener, seat); } @@ -895,6 +924,20 @@ handle_global(void *data, struct wl_registry *registry, wayl->presentation, &presentation_listener, wayl); } } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->text_input_manager = wl_registry_bind( + wayl->registry, name, &zwp_text_input_manager_v3_interface, required); + + tll_foreach(wayl->seats, it) + seat_add_text_input(&it->item); + } +#endif } static void @@ -1154,6 +1197,11 @@ wayl_destroy(struct wayland *wayl) seat_destroy(&it->item); tll_free(wayl->seats); +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (wayl->text_input_manager != NULL) + zwp_text_input_manager_v3_destroy(wayl->text_input_manager); +#endif + if (wayl->xdg_output_manager != NULL) zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); if (wayl->shell != NULL) diff --git a/wayland.h b/wayland.h index faee928e..f275c69c 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 { @@ -220,6 +223,35 @@ struct seat { struct wl_clipboard clipboard; struct wl_primary primary; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + /* Input Method Editor */ + struct zwp_text_input_v3 *wl_text_input; + struct { + struct { + struct { + char *text; + int32_t cursor_begin; + int32_t cursor_end; + } pending; + } preedit; + + struct { + struct { + char *text; + } pending; + } commit; + + struct { + struct { + uint32_t before_length; + uint32_t after_length; + } pending; + } surrounding; + + uint32_t serial; + } ime; +#endif }; enum csd_surface { @@ -374,6 +406,10 @@ struct wayland { struct wp_presentation *presentation; uint32_t presentation_clock_id; +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + struct zwp_text_input_manager_v3 *text_input_manager; +#endif + bool have_argb8888; tll(struct monitor) monitors; /* All available outputs */ tll(struct seat) seats;