From 148bb1ff136ee225aaa93ff9b40700feb81503c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 10 Oct 2020 22:14:35 +0200 Subject: [PATCH 01/46] ime: wip: add text-input object to seat --- ime.c | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++ ime.h | 5 +++ meson.build | 2 ++ wayland.c | 36 ++++++++++++++++++++ wayland.h | 7 ++++ 5 files changed, 147 insertions(+) create mode 100644 ime.c create mode 100644 ime.h diff --git a/ime.c b/ime.c new file mode 100644 index 00000000..baac7db5 --- /dev/null +++ b/ime.c @@ -0,0 +1,97 @@ +#include "ime.h" + +#include "text-input-unstable-v3.h" + +#define LOG_MODULE "ime" +#define LOG_ENABLE_DBG 1 +#include "log.h" +#include "terminal.h" +#include "wayland.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); + + assert(seat->kbd_focus != NULL); + + switch (term_surface_kind(seat->kbd_focus, surface)) { + case TERM_SURF_GRID: + 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); + + /* TODO: set cursor rectangle */ + zwp_text_input_v3_set_cursor_rectangle(seat->wl_text_input, 0, 0, 15, 15); + break; + + case TERM_SURF_SEARCH: + /* TODO */ + /* FALLTHROUGH */ + + case TERM_SURF_NONE: + case TERM_SURF_SCROLLBACK_INDICATOR: + case TERM_SURF_RENDER_TIMER: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + zwp_text_input_v3_disable(seat->wl_text_input); + break; + } + + zwp_text_input_v3_commit(seat->wl_text_input); +} + +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); +} + +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-strig: text=%s, begin=%d, end=%d", text, cursor_begin, 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); +} + +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); +} + +static void +done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t serial) +{ + LOG_DBG("done: serial=%u", 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, +}; diff --git a/ime.h b/ime.h new file mode 100644 index 00000000..d1ce7ff0 --- /dev/null +++ b/ime.h @@ -0,0 +1,5 @@ +#pragma once + +#include "text-input-unstable-v3.h" + +extern const struct zwp_text_input_v3_listener text_input_listener; diff --git a/meson.build b/meson.build index 6b9a8c4d..1aa018a9 100644 --- a/meson.build +++ b/meson.build @@ -78,6 +78,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 +148,7 @@ executable( 'commands.c', 'commands.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', + 'ime.c', 'ime.h', 'input.c', 'input.h', 'main.c', 'quirks.c', 'quirks.h', diff --git a/wayland.c b/wayland.c index 8007eeba..2f7b3b95 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,23 @@ 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 (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); +} + static void seat_destroy(struct seat *seat) { @@ -165,6 +184,8 @@ seat_destroy(struct seat *seat) wl_keyboard_release(seat->wl_keyboard); if (seat->wl_pointer != NULL) wl_pointer_release(seat->wl_pointer); + if (seat->wl_text_input != NULL) + zwp_text_input_v3_destroy(seat->wl_text_input); if (seat->wl_seat != NULL) wl_seat_release(seat->wl_seat); @@ -815,6 +836,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 +917,18 @@ handle_global(void *data, struct wl_registry *registry, wayl->presentation, &presentation_listener, wayl); } } + + 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); + } } static void @@ -1154,6 +1188,8 @@ wayl_destroy(struct wayland *wayl) seat_destroy(&it->item); tll_free(wayl->seats); + if (wayl->text_input_manager != NULL) + zwp_text_input_manager_v3_destroy(wayl->text_input_manager); 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..e006b395 100644 --- a/wayland.h +++ b/wayland.h @@ -220,6 +220,11 @@ struct seat { struct wl_clipboard clipboard; struct wl_primary primary; + + /* Input Method Editor */ + struct zwp_text_input_v3 *wl_text_input; + //struct { + //} ime; }; enum csd_surface { @@ -374,6 +379,8 @@ struct wayland { struct wp_presentation *presentation; uint32_t presentation_clock_id; + struct zwp_text_input_manager_v3 *text_input_manager; + bool have_argb8888; tll(struct monitor) monitors; /* All available outputs */ tll(struct seat) seats; From e97024116e0efcf956fd7504aeee70bd3ee0d999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 28 Nov 2020 21:59:58 +0100 Subject: [PATCH 02/46] ime: disable text-input on surface leave --- ime.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ime.c b/ime.c index baac7db5..1619e584 100644 --- a/ime.c +++ b/ime.c @@ -48,7 +48,7 @@ enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, break; } - zwp_text_input_v3_commit(seat->wl_text_input); + zwp_text_input_v3_commit(seat->wl_text_input); } static void @@ -57,13 +57,15 @@ leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, { struct seat *seat = data; LOG_DBG("leave: seat=%s", seat->name); + zwp_text_input_v3_disable(seat->wl_text_input); + zwp_text_input_v3_commit(seat->wl_text_input); } 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-strig: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end); + LOG_DBG("preedit-string: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end); } static void From b85e2f40bbc45709e8f47a435ff458f054832a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 28 Nov 2020 22:00:26 +0100 Subject: [PATCH 03/46] ime: feed committed string to client application --- ime.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ime.c b/ime.c index 1619e584..eda975c5 100644 --- a/ime.c +++ b/ime.c @@ -1,5 +1,7 @@ #include "ime.h" +#include + #include "text-input-unstable-v3.h" #define LOG_MODULE "ime" @@ -72,7 +74,9 @@ static void commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, const char *text) { + struct seat *seat = data; LOG_DBG("commit: text=%s", text); + term_to_slave(seat->kbd_focus, text, strlen(text)); } static void From 5745c610ac5e9b96f9bac8347c6a5645d89a66c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 1 Dec 2020 19:31:49 +0100 Subject: [PATCH 04/46] =?UTF-8?q?ime:=20wip:=20commit=20all=20changes=20in?= =?UTF-8?q?=20=E2=80=98done()=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ime.c | 42 ++++++++++++++++++++++++++++++++++++++++-- input.c | 4 +++- vt.c | 2 +- wayland.c | 2 ++ wayland.h | 20 ++++++++++++++++++-- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/ime.c b/ime.c index eda975c5..9bcf0a9f 100644 --- a/ime.c +++ b/ime.c @@ -9,6 +9,7 @@ #include "log.h" #include "terminal.h" #include "wayland.h" +#include "xmalloc.h" static void enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, @@ -51,6 +52,7 @@ enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, } zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; } static void @@ -61,6 +63,7 @@ leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, LOG_DBG("leave: seat=%s", seat->name); zwp_text_input_v3_disable(seat->wl_text_input); zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; } static void @@ -68,15 +71,23 @@ 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; } static void commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, const char *text) { - struct seat *seat = data; LOG_DBG("commit: text=%s", text); - term_to_slave(seat->kbd_focus, text, strlen(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)); } static void @@ -84,13 +95,40 @@ 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; } static void done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t serial) { + /* + * 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_WARN("IME serial mismatch: expected=0x%08x, got 0x%08x", + seat->ime.serial, serial); + + assert(seat->kbd_focus); + + if (seat->ime.commit.text != NULL) + term_to_slave(seat->kbd_focus, seat->ime.commit.text, strlen(seat->ime.commit.text)); + + free(seat->ime.commit.text); + seat->ime.commit.text = NULL; } const struct zwp_text_input_v3_listener text_input_listener = { diff --git a/input.c b/input.c index 31c1d816..4abbb89e 100644 --- a/input.c +++ b/input.c @@ -21,7 +21,7 @@ #include #define LOG_MODULE "input" -#define LOG_ENABLE_DBG 0 +#define LOG_ENABLE_DBG 1 #include "log.h" #include "config.h" #include "commands.h" @@ -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/vt.c b/vt.c index ed26da73..1522dacf 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 2f7b3b95..ecaecb1c 100644 --- a/wayland.c +++ b/wayland.c @@ -191,6 +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->name); } diff --git a/wayland.h b/wayland.h index e006b395..b29bb8ab 100644 --- a/wayland.h +++ b/wayland.h @@ -223,8 +223,24 @@ struct seat { /* Input Method Editor */ struct zwp_text_input_v3 *wl_text_input; - //struct { - //} ime; + struct { + struct { + char *text; + int32_t cursor_begin; + int32_t cursor_end; + } preedit; + + struct { + char *text; + } commit; + + struct { + uint32_t before_length; + uint32_t after_length; + } surrounding; + + uint32_t serial; + } ime; }; enum csd_surface { 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 05/46] 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; From 05083110c3fad046ccf0838a364150b8d2bab888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:36:56 +0100 Subject: [PATCH 06/46] ime: make IME compile-time optional --- ime.c | 63 ++++++++++++++++++++++++++++++++++------------- ime.h | 10 ++++++++ meson.build | 15 ++++++----- meson_options.txt | 1 + render.c | 3 +++ terminal.c | 18 +++++++++++--- terminal.h | 4 +++ wayland.c | 14 +++++++++-- wayland.h | 4 +++ 9 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 meson_options.txt diff --git a/ime.c b/ime.c index 787985cf..ccdbb098 100644 --- a/ime.c +++ b/ime.c @@ -1,5 +1,7 @@ #include "ime.h" +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + #include #include "text-input-unstable-v3.h" @@ -67,11 +69,7 @@ leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, 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; + ime_reset(seat); } static void @@ -82,10 +80,13 @@ preedit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, struct seat *seat = data; - 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; + 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 @@ -95,8 +96,11 @@ commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, LOG_DBG("commit: text=%s", text); struct seat *seat = data; - free(seat->ime.commit.pending.text); - seat->ime.commit.pending.text = text != NULL ? xstrdup(text) : NULL; + + ime_reset_commit(seat); + + if (text != NULL) + seat->ime.commit.pending.text = xstrdup(text); } static void @@ -142,9 +146,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 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; + term_reset_ime(term); render_refresh(term); } @@ -162,9 +164,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, term, seat->ime.commit.pending.text, strlen(seat->ime.commit.pending.text)); - - free(seat->ime.commit.pending.text); - seat->ime.commit.pending.text = NULL; + ime_reset_commit(seat); } /* 4. Calculate surrounding text to send - not supported */ @@ -292,10 +292,31 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, render_refresh(term); } + ime_reset_preedit(seat); +} + +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); +} + + const struct zwp_text_input_v3_listener text_input_listener = { .enter = &enter, .leave = &leave, @@ -304,3 +325,11 @@ const struct zwp_text_input_v3_listener text_input_listener = { .delete_surrounding_text = &delete_surrounding_text, .done = &done, }; + +#else /* !FOOT_IME_ENABLED */ + +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 index d1ce7ff0..d5c96534 100644 --- a/ime.h +++ b/ime.h @@ -1,5 +1,15 @@ #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_reset_preedit(struct seat *seat); +void ime_reset_commit(struct seat *seat); +void ime_reset(struct seat *seat); diff --git a/meson.build b/meson.build index 1aa018a9..6e04ba8b 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', @@ -198,9 +201,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/render.c b/render.c index 88a47d47..3e7ed96a 100644 --- a/render.c +++ b/render.c @@ -1025,6 +1025,8 @@ static void render_ime_preedit(struct terminal *term, struct buffer *buf, struct coord cursor) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (likely(term->ime.preedit.cells == NULL)) return; @@ -1112,6 +1114,7 @@ render_ime_preedit(struct terminal *term, struct buffer *buf, term->margins.top + row_idx * term->cell_height, term->width - term->margins.left - term->margins.right, 1 * term->cell_height); +#endif } static void diff --git a/terminal.c b/terminal.c index d08d8658..cfcc2040 100644 --- a/terminal.c +++ b/terminal.c @@ -1403,7 +1403,7 @@ term_destroy(struct terminal *term) tll_free(term->alt.sixel_images); sixel_fini(term); - free(term->ime.preedit.cells); + term_reset_ime(term); free(term->foot_exe); free(term->cwd); @@ -2218,12 +2218,12 @@ 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) { - free(term->ime.preedit.cells); - term->ime.preedit.cells = NULL; - term->ime.preedit.count = 0; + term_reset_ime(term); render_refresh(term); } +#endif term->kbd_focus = false; cursor_refresh(term); @@ -2753,3 +2753,13 @@ 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); } + +void +term_reset_ime(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + free(term->ime.preedit.cells); + term->ime.preedit.cells = NULL; + term->ime.preedit.count = 0; +#endif +} diff --git a/terminal.h b/terminal.h index b8339d8d..bbe7df25 100644 --- a/terminal.h +++ b/terminal.h @@ -470,6 +470,7 @@ struct terminal { unsigned max_height; /* Maximum image height, in pixels */ } sixel; +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct { struct { struct cell *cells; @@ -482,6 +483,7 @@ struct terminal { } cursor; } preedit; } ime; +#endif bool quit; bool is_shutting_down; @@ -605,3 +607,5 @@ 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); + +void term_reset_ime(struct terminal *term); diff --git a/wayland.c b/wayland.c index c0a9c512..c8cb2c9c 100644 --- a/wayland.c +++ b/wayland.c @@ -116,6 +116,7 @@ seat_add_primary_selection(struct seat *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; @@ -128,6 +129,7 @@ seat_add_text_input(struct seat *seat) seat->wl_text_input = text_input; zwp_text_input_v3_add_listener(text_input, &text_input_listener, seat); +#endif } static void @@ -184,15 +186,18 @@ 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->ime.preedit.pending.text); - free(seat->ime.commit.pending.text); free(seat->name); } @@ -920,6 +925,7 @@ handle_global(void *data, struct wl_registry *registry, } } +#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)) @@ -931,6 +937,7 @@ handle_global(void *data, struct wl_registry *registry, tll_foreach(wayl->seats, it) seat_add_text_input(&it->item); } +#endif } static void @@ -1190,8 +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 51a6fad1..f275c69c 100644 --- a/wayland.h +++ b/wayland.h @@ -224,6 +224,7 @@ 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 { @@ -250,6 +251,7 @@ struct seat { uint32_t serial; } ime; +#endif }; enum csd_surface { @@ -404,7 +406,9 @@ 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 */ From 4d90b200f1252e691592c98439635b7a0b2f6592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:37:12 +0100 Subject: [PATCH 07/46] render: ignore IME preedit state when deciding which cursor style to render --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 3e7ed96a..4d1e1465 100644 --- a/render.c +++ b/render.c @@ -353,7 +353,7 @@ draw_cursor(const struct terminal *term, const struct cell *cell, switch (term->cursor_style) { case CURSOR_BLOCK: - if (unlikely(!term->kbd_focus || term->ime.preedit.cells != NULL)) + if (unlikely(!term->kbd_focus)) draw_unfocused_block(term, pix, &cursor_color, x, y, cols); else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) { From 98bd798daed897c56b6f0262e5105131a074bdad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:37:40 +0100 Subject: [PATCH 08/46] ime: calculate wchar widths once --- ime.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ime.c b/ime.c index ccdbb098..bde063a8 100644 --- a/ime.c +++ b/ime.c @@ -181,8 +181,13 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 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); + size_t widths[wchars + 1]; + + for (size_t i = 0; i < wchars; i++) { + int width = max(wcwidth(wcs[i]), 1); + widths[i] = width; + cell_count += width; + } /* Allocate cells */ term->ime.preedit.cells = xmalloc( @@ -193,7 +198,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, 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); + int width = widths[i]; cell->wc = wcs[i]; cell->attrs = (struct attributes){.clean = 0, .underline = 1}; From e9f99df2ab028d4385bef303bb5d77dab7b12e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:38:26 +0100 Subject: [PATCH 09/46] render: ime: calculate on-screen cursor position ourselves The position calculated by render_grid() may be -1,-1 if the cursor is currently hidden. This fixes a crash when trying to input IME while the cursor is hidden. --- render.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/render.c b/render.c index 4d1e1465..9546de46 100644 --- a/render.c +++ b/render.c @@ -1022,14 +1022,23 @@ render_sixel_images(struct terminal *term, pixman_image_t *pix) } static void -render_ime_preedit(struct terminal *term, struct buffer *buf, - struct coord cursor) +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; + /* 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; @@ -2012,7 +2021,7 @@ grid_render(struct terminal *term) } /* Render IME pre-edit text */ - render_ime_preedit(term, buf, cursor); + render_ime_preedit(term, buf); if (term->flash.active) { /* Note: alpha is pre-computed in each color component */ From 83d3ae10aeea5ac7d451d789891542a1964c3faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:39:34 +0100 Subject: [PATCH 10/46] =?UTF-8?q?render:=20ime:=20don=E2=80=99t=20render?= =?UTF-8?q?=20cursor=20if=20cursor=20begin=20=3D=3D=20end?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/render.c b/render.c index 9546de46..34323801 100644 --- a/render.c +++ b/render.c @@ -1093,9 +1093,10 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) } /* Hollow cursor */ - if (!term->ime.preedit.cursor.hidden) { - int start = term->ime.preedit.cursor.start; - int end = term->ime.preedit.cursor.end; + int start = term->ime.preedit.cursor.start; + int end = term->ime.preedit.cursor.end; + + if (!term->ime.preedit.cursor.hidden && end > start) { pixman_color_t fg = color_hex_to_pixman(term->colors.fg); pixman_color_t bg = color_hex_to_pixman(term->colors.bg); From cc76f91c43981cb95e508c5b90c71bec8bbcab76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:39:53 +0100 Subject: [PATCH 11/46] ime: use correct index when calculating number of *cells* to advance --- ime.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ime.c b/ime.c index bde063a8..970efc81 100644 --- a/ime.c +++ b/ime.c @@ -269,7 +269,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, break; byte_idx += wc_bytes; - cell_idx += max(wcwidth(term->ime.preedit.cells[wc_idx].wc), 1); + cell_idx += max(wcwidth(term->ime.preedit.cells[cell_idx].wc), 1); wc_idx++; } From 552b9884b905fc136e368344da313eabe7bad858 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:40:18 +0100 Subject: [PATCH 12/46] ime: set cursor end correctly when it ends *after* the pre-edit string --- ime.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ime.c b/ime.c index 970efc81..67718517 100644 --- a/ime.c +++ b/ime.c @@ -273,6 +273,9 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, wc_idx++; } + 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); From 001c82fa547bb8c62c50bde1b17ff9c8dd3d61c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:40:54 +0100 Subject: [PATCH 13/46] =?UTF-8?q?ime:=20don=E2=80=99t=20adjust=20cursor?= =?UTF-8?q?=E2=80=99s=20cell-end=20if=20it=E2=80=99s=20equal=20to=20the=20?= =?UTF-8?q?cell-begin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ime.c | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ime.c b/ime.c index 67718517..eb4eadf4 100644 --- a/ime.c +++ b/ime.c @@ -278,19 +278,25 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 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); +#if 1 + if (cell_end < cell_begin) + cell_end = cell_begin; +#else + if (cell_end <= cell_begin) { + cell_end = cell_begin + max( + wcwidth(term->ime.preedit.cells[cell_begin].wc), 1); + } +#endif + 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_begin <= cell_end); + assert(cell_end >= 0); assert(cell_end <= cell_count); - assert(cell_begin < cell_end); term->ime.preedit.cursor.hidden = false; term->ime.preedit.cursor.start = cell_begin; From 5c17b7f8e77f96622d558ccbd5071706863c2816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Thu, 3 Dec 2020 18:41:42 +0100 Subject: [PATCH 14/46] ime: expand cursor cell-end across multi-cell characters This fixes an issue where the cursor ended in the middle of e.g double width characters. --- ime.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ime.c b/ime.c index eb4eadf4..53a908b0 100644 --- a/ime.c +++ b/ime.c @@ -290,6 +290,13 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, } #endif + /* 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); From b59d695b2b00157e403399af78b3128848a65e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 18:39:11 +0100 Subject: [PATCH 15/46] ime: add functions to enable/disable IME, simplify code that enables IME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We may want to be able to enable/disable IME run-time, even though we have received an ‘enter’ IME event. This enables us to do that. Also add functions to enable/disable IME on a per-terminal instance basis. A terminal may have multiple seats focusing it, and enabling/disabling IME in a terminal instance enables/disables IME on all those seats. Finally, the code to enable IME is simplified; the *only* surface that can ever receive ‘enter’ IME events is the main grid. All other surfaces are sub-surfaces, without their own keyboard focus. --- ime.c | 82 ++++++++++++++++++++++++++++-------------------------- ime.h | 3 ++ pgo/pgo.c | 3 ++ terminal.c | 76 +++++++++++++++++++++++++++++++++++++++++++++----- terminal.h | 6 +++- 5 files changed, 122 insertions(+), 48 deletions(-) diff --git a/ime.c b/ime.c index 53a908b0..a60e72f5 100644 --- a/ime.c +++ b/ime.c @@ -20,43 +20,16 @@ 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); - assert(seat->kbd_focus != NULL); + /* 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); - switch (term_surface_kind(seat->kbd_focus, surface)) { - case TERM_SURF_GRID: - 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); - - /* TODO: set cursor rectangle */ - zwp_text_input_v3_set_cursor_rectangle(seat->wl_text_input, 0, 0, 15, 15); - break; - - case TERM_SURF_SEARCH: - /* TODO */ - /* FALLTHROUGH */ - - case TERM_SURF_NONE: - case TERM_SURF_SCROLLBACK_INDICATOR: - case TERM_SURF_RENDER_TIMER: - case TERM_SURF_TITLE: - case TERM_SURF_BORDER_LEFT: - case TERM_SURF_BORDER_RIGHT: - case TERM_SURF_BORDER_TOP: - case TERM_SURF_BORDER_BOTTOM: - case TERM_SURF_BUTTON_MINIMIZE: - case TERM_SURF_BUTTON_MAXIMIZE: - case TERM_SURF_BUTTON_CLOSE: - zwp_text_input_v3_disable(seat->wl_text_input); - break; - } - - zwp_text_input_v3_commit(seat->wl_text_input); - seat->ime.serial++; + ime_enable(seat); } static void @@ -65,11 +38,7 @@ leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, { struct seat *seat = data; LOG_DBG("leave: seat=%s", seat->name); - zwp_text_input_v3_disable(seat->wl_text_input); - zwp_text_input_v3_commit(seat->wl_text_input); - seat->ime.serial++; - - ime_reset(seat); + ime_disable(seat); } static void @@ -146,7 +115,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 1. Delete existing pre-edit text */ if (term->ime.preedit.cells != NULL) { - term_reset_ime(term); + term_ime_reset(term); render_refresh(term); } @@ -337,6 +306,36 @@ ime_reset(struct seat *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, @@ -349,6 +348,9 @@ const struct zwp_text_input_v3_listener text_input_listener = { #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) {} diff --git a/ime.h b/ime.h index d5c96534..7036bbde 100644 --- a/ime.h +++ b/ime.h @@ -10,6 +10,9 @@ extern const struct zwp_text_input_v3_listener text_input_listener; 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/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/terminal.c b/terminal.c index cfcc2040..80c162bd 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,7 +1409,7 @@ term_destroy(struct terminal *term) tll_free(term->alt.sixel_images); sixel_fini(term); - term_reset_ime(term); + term_ime_reset(term); free(term->foot_exe); free(term->cwd); @@ -1559,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; @@ -2220,7 +2230,7 @@ term_kbd_focus_out(struct terminal *term) #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (term->ime.preedit.cells != NULL) { - term_reset_ime(term); + term_ime_reset(term); render_refresh(term); } #endif @@ -2754,12 +2764,64 @@ term_view_to_text(const struct terminal *term, char **text, size_t *len) return rows_to_text(term, start, end, text, len); } -void -term_reset_ime(struct terminal *term) +bool +term_ime_is_enabled(const struct terminal *term) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - free(term->ime.preedit.cells); - term->ime.preedit.cells = NULL; - term->ime.preedit.count = 0; + 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 currenly 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 currenly 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.cells); + term->ime.preedit.cells = NULL; + term->ime.preedit.count = 0; + } #endif } diff --git a/terminal.h b/terminal.h index bbe7df25..6a634914 100644 --- a/terminal.h +++ b/terminal.h @@ -472,6 +472,7 @@ struct terminal { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct { + bool enabled; struct { struct cell *cells; int count; @@ -608,4 +609,7 @@ bool term_scrollback_to_text( bool term_view_to_text( const struct terminal *term, char **text, size_t *len); -void term_reset_ime(struct terminal *term); +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); From 85cdc66ff2e11704ab33f27095681ec886f6d721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 18:42:16 +0100 Subject: [PATCH 16/46] shm: fix badly indented function return type --- shm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); From 2078e1675d8ec1e1c90e1ce7063d0a70d54ea167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 18:43:06 +0100 Subject: [PATCH 17/46] =?UTF-8?q?render:=20ime:=20draw=20a=20=E2=80=98bar?= =?UTF-8?q?=E2=80=99=20cursor=20when=20the=20pre-edit=20cursor=E2=80=99s?= =?UTF-8?q?=20begin=20=3D=3D=20end?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/render.c b/render.c index 34323801..2a3ab5b8 100644 --- a/render.c +++ b/render.c @@ -1092,25 +1092,33 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false); } - /* Hollow cursor */ int start = term->ime.preedit.cursor.start; int end = term->ime.preedit.cursor.end; - if (!term->ime.preedit.cursor.hidden && end > start) { + if (!term->ime.preedit.cursor.hidden) { + const struct cell *start_cell = &term->ime.preedit.cells[start]; 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); + 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; - int cols = end - start; - draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + if (end == start) { + /* Bar */ + 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 */ + int cols = end - start; + draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + } } /* Restore original content (but do not render) */ From 77bc7c8b2c61c9eb166fa0050be0b53270475e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 18:57:49 +0100 Subject: [PATCH 18/46] features: include compile-time enable/disable state of features when printing version --- client.c | 12 +++++++++++- foot-features.h | 12 ++++++++++++ main.c | 14 ++++++++++++-- meson.build | 2 ++ 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 foot-features.h 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/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/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 6e04ba8b..36c5807e 100644 --- a/meson.build +++ b/meson.build @@ -151,6 +151,7 @@ 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', @@ -174,6 +175,7 @@ executable( executable( 'footclient', 'client.c', 'client-protocol.h', + 'foot-features.h', 'log.c', 'log.h', 'macros.h', 'xmalloc.c', 'xmalloc.h', From 0fc2a1188c2d7400be63debaf16339af49fa804a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 20:08:22 +0100 Subject: [PATCH 19/46] codespell: currenly -> currently --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 80c162bd..32395d76 100644 --- a/terminal.c +++ b/terminal.c @@ -2786,7 +2786,7 @@ term_ime_enable(struct terminal *term) term->ime.enabled = true; term_ime_reset(term); - /* IME is per seat - enable on all seat currenly focusing us */ + /* 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); From e3b5a98ade9817a0288bdd4b78263c9961e77241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 20:33:15 +0100 Subject: [PATCH 20/46] codespell: currenly -> currently, second instance --- terminal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal.c b/terminal.c index 32395d76..215304ec 100644 --- a/terminal.c +++ b/terminal.c @@ -2806,7 +2806,7 @@ term_ime_disable(struct terminal *term) term->ime.enabled = false; term_ime_reset(term); - /* IME is per seat - disable on all seat currenly focusing us */ + /* 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); From 0536bc41f4c3538aa1ad4cd68a5339de6a5a8613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 21:26:38 +0100 Subject: [PATCH 21/46] csi: add DECSET 737769 - enables/disables IME input 73 77 69 = I M E --- csi.c | 9 +++++++++ terminal.h | 1 + 2 files changed, 10 insertions(+) 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/terminal.h b/terminal.h index 6a634914..f99c77c0 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; From 3df4ec1c8e521f62d78d1e91ce1571aa669f306e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Fri, 4 Dec 2020 22:20:02 +0100 Subject: [PATCH 22/46] changelog: IME support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac69ddd3..2ff066e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Added +* IME support (https://codeberg.org/dnkl/foot/issues/134). * 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 From 559c87b84a03ce5bc2338d51bd5995562338d4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:06:50 +0100 Subject: [PATCH 23/46] =?UTF-8?q?install:=20add=20=E2=80=98options?= =?UTF-8?q?=E2=80=99=20section,=20describing=20compile-time=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- INSTALL.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/INSTALL.md b/INSTALL.md index 40e167a2..45ab5ff1 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 + +All variants below support the following 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 From 725d17d21d0e6ea042f34d12bd17c8c022a049c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:09:05 +0100 Subject: [PATCH 24/46] changelog: ime: add link to INSTALL.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff066e8..fbb320d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See ### Added -* IME support (https://codeberg.org/dnkl/foot/issues/134). +* IME support. This is compile-time optional, see + [INSTALL.md](INSTALL.md#user-content-options) + (https://codeberg.org/dnkl/foot/issues/134). * 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 From ab699338b3c81e62c6176a5a5be9852050e321e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:09:55 +0100 Subject: [PATCH 25/46] install: highlight option name in options list --- INSTALL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 45ab5ff1..2afab80e 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -135,9 +135,9 @@ mkdir -p bld/release && cd bld/release All variants below support the following compile-time options: -| Option | Type | Default | Description | Extra dependencies | -|--------|------|---------|---------------------|--------------------| -| -Dime | Bool | True | Enables IME support | None | +| Option | Type | Default | Description | Extra dependencies | +|----------|------|---------|---------------------|--------------------| +| `-Dime` | Bool | True | Enables IME support | None | ### Release build From fe6cf547f15c3f85d97901446c9082aca69af6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:10:29 +0100 Subject: [PATCH 26/46] install: highlight option default value in options list --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index 2afab80e..c370347c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -137,7 +137,7 @@ All variants below support the following compile-time options: | Option | Type | Default | Description | Extra dependencies | |----------|------|---------|---------------------|--------------------| -| `-Dime` | Bool | True | Enables IME support | None | +| `-Dime` | bool | `true` | Enables IME support | None | ### Release build From 9ce0edc8bbcbdc5d269bdf573e20d959878ffa85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:42:21 +0100 Subject: [PATCH 27/46] =?UTF-8?q?render:=20ime:=20don=E2=80=99t=20render?= =?UTF-8?q?=20pre-edit=20string=20in=20grid=20while=20searching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While doing a scrollback search, the pre-edit string should be rendered in the search box, not in the grid. Note that we don’t yet support IME in scrollback search mode. This patch simply prevents the pre-edit text being rendered in the grid, in the “background”, while searching. --- render.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/render.c b/render.c index 2a3ab5b8..24e4c6c4 100644 --- a/render.c +++ b/render.c @@ -1029,6 +1029,9 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) 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; From 7eea6f94ed753dd358fb53dac889fc40455810e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:43:54 +0100 Subject: [PATCH 28/46] =?UTF-8?q?ime:=20don=E2=80=99t=20commit=20to=20the?= =?UTF-8?q?=20slave=20while=20scrollback=20searching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ime.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ime.c b/ime.c index a60e72f5..17db0b1a 100644 --- a/ime.c +++ b/ime.c @@ -129,10 +129,14 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 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)); + if (term->is_searching) { + /* TODO */ + } else { + term_to_slave( + term, + seat->ime.commit.pending.text, + strlen(seat->ime.commit.pending.text)); + } ime_reset_commit(seat); } @@ -214,7 +218,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, * * 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; From f51ce347532d0c0c0169ba6e1d32a09b13481ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:49:32 +0100 Subject: [PATCH 29/46] search: new function: search_add_chars() This function inserts a string into the search buffer, at the current insertion point (i.e. where the cursor is). --- search.c | 8 ++++---- search.h | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/search.c b/search.c index bd5effce..a9f5499a 100644 --- a/search.c +++ b/search.c @@ -332,8 +332,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 +494,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 +722,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); From d87160e5f679fc8d9c53d21584932368add61c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:50:27 +0100 Subject: [PATCH 30/46] ime: commit text using search_add_chars() while in scrollback search mode --- ime.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ime.c b/ime.c index 17db0b1a..9c6676ab 100644 --- a/ime.c +++ b/ime.c @@ -10,6 +10,7 @@ #define LOG_ENABLE_DBG 0 #include "log.h" #include "render.h" +#include "search.h" #include "terminal.h" #include "util.h" #include "wayland.h" @@ -129,14 +130,14 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 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) { - /* TODO */ - } else { - term_to_slave( - term, - seat->ime.commit.pending.text, - strlen(seat->ime.commit.pending.text)); - } + search_add_chars(term, text, len); + render_refresh_search(term); + } else + term_to_slave(term, text, len); ime_reset_commit(seat); } From dca35215d079d11f0138a4cd2c98b76b39524a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:59:41 +0100 Subject: [PATCH 31/46] render: search: render colorized glyphs (emojis) correctly --- render.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/render.c b/render.c index 24e4c6c4..565e8878 100644 --- a/render.c +++ b/render.c @@ -2184,12 +2184,20 @@ render_search_box(struct terminal *term) if (glyph == NULL) 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, + 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); + pixman_image_unref(src); + } x += term->cell_width; } From a552610fdb721980bc455c7dc8636bc13d8f698b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 11:59:56 +0100 Subject: [PATCH 32/46] render: search: handle multi-column characters --- render.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/render.c b/render.c index 565e8878..2a8a50dd 100644 --- a/render.c +++ b/render.c @@ -2117,7 +2117,8 @@ render_search_box(struct terminal *term) { assert(term->window->search_sub_surface != NULL); - const size_t wanted_visible_chars = max(20, term->search.len); + const size_t wanted_visible_chars = max( + 20, wcsnlen(term->search.buf, term->search.len)); assert(term->scale >= 1); const int scale = term->scale; @@ -2199,7 +2200,7 @@ render_search_box(struct terminal *term) pixman_image_unref(src); } - x += term->cell_width; + x += max(1, wcwidth(term->search.buf[i])) * term->cell_width; } if (term->search.cursor >= term->search.len) From 5ffa65a2e307e2442303d9d0ff2c2e05a1a8eca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 22:51:00 +0100 Subject: [PATCH 33/46] render: search: wcsnlen() -> wcswidth() --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 2a8a50dd..9cbe85d6 100644 --- a/render.c +++ b/render.c @@ -2118,7 +2118,7 @@ render_search_box(struct terminal *term) assert(term->window->search_sub_surface != NULL); const size_t wanted_visible_chars = max( - 20, wcsnlen(term->search.buf, term->search.len)); + 20, wcswidth(term->search.buf, term->search.len)); assert(term->scale >= 1); const int scale = term->scale; From b4a0f5b13b44525bd1dbdbc12e4e412fc9a0a1cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 23:29:12 +0100 Subject: [PATCH 34/46] render: search: glyph_offset is in *cells*, cursor position in *characters* When calculating the offset into the search string, from where to start rendering, take into account that the cursor position is in *characters*, and the glyph-offset is in *cells*. --- render.c | 82 +++++++++++++++++++++++++++++++++++++++++++------------- search.c | 6 +++++ 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/render.c b/render.c index 9cbe85d6..7fdade1a 100644 --- a/render.c +++ b/render.c @@ -2117,8 +2117,19 @@ render_search_box(struct terminal *term) { assert(term->window->search_sub_surface != NULL); - const size_t wanted_visible_chars = max( - 20, wcswidth(term->search.buf, 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 cound to cell count when calculating the length of + * the search box, where in the search string we should start + * rendering etc. + */ + + const size_t total_cells = wcswidth(term->search.buf, term->search.len); + const size_t wanted_visible_cells = max(20, total_cells); assert(term->scale >= 1); const int scale = term->scale; @@ -2128,12 +2139,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); @@ -2158,32 +2169,66 @@ render_search_box(struct terminal *term) 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); + /* + * Ensure cursor is visible + * + * First, we need to map the cursor character position to a cell + * position. Then we can ensure the cursor is within the rendered + * part of the search string. + */ + for (size_t i = 0, cell_idx = 0; + i <= term->search.cursor; + cell_idx += max(1, wcwidth(term->search.buf[i])), i++) + { + if (i != term->search.cursor) + continue; + + if (cell_idx < glyph_offset) + term->render.search_glyph_offset = glyph_offset = cell_idx; + else if (cell_idx > glyph_offset + visible_cells) { + term->render.search_glyph_offset = glyph_offset = + cell_idx - min(cell_idx, visible_cells); + } + break; } /* Move offset if there is free space available */ - if (term->search.len - glyph_offset < visible_chars) + if (total_cells - glyph_offset < visible_cells) term->render.search_glyph_offset = glyph_offset = - term->search.len - min(term->search.len, visible_chars); + total_cells - min(total_cells, visible_cells); - /* Text (what the user entered - *not* match(es)) */ - for (size_t i = glyph_offset; - i < term->search.len && i - glyph_offset < visible_chars; - i++) + /* + * 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 = max(1, wcwidth(term->search.buf[i])), + next_cell_idx = width; + i < term->search.len; + i++, + cell_idx = next_cell_idx, + width = max(1, wcwidth(term->search.buf[i])), + next_cell_idx += width) { if (i == term->search.cursor) draw_bar(term, buf->pix[0], font, &fg, x, y); + if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) + break; + + if (cell_idx < glyph_offset) { + cell_idx = next_cell_idx; + continue; + } + const struct fcft_glyph *glyph = fcft_glyph_rasterize( font, term->search.buf[i], term->font_subpixel); - if (glyph == NULL) + 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...) */ @@ -2200,7 +2245,8 @@ render_search_box(struct terminal *term) pixman_image_unref(src); } - x += max(1, wcwidth(term->search.buf[i])) * term->cell_width; + x += width * term->cell_width; + cell_idx = next_cell_idx; } if (term->search.cursor >= term->search.len) diff --git a/search.c b/search.c index a9f5499a..83f86390 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. @@ -124,6 +125,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); } From 1ea36740290162dfda5659b04ac364b0272a3423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 23:34:27 +0100 Subject: [PATCH 35/46] render: codespell: cound -> count --- render.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render.c b/render.c index 7fdade1a..0e251e60 100644 --- a/render.c +++ b/render.c @@ -2123,7 +2123,7 @@ render_search_box(struct terminal *term) * * The search ‘length’, and ‘cursor’ (position) is in * *characters*, not cells. This means we need to translate from - * character cound to cell count when calculating the length of + * character count to cell count when calculating the length of * the search box, where in the search string we should start * rendering etc. */ From 7c420004fb2db841b8c800494c865e14b3ecaffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sat, 5 Dec 2020 23:34:42 +0100 Subject: [PATCH 36/46] =?UTF-8?q?render:=20search:=20don=E2=80=99t=20acces?= =?UTF-8?q?s=20term->search.buf[]=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/render.c b/render.c index 0e251e60..04f523cd 100644 --- a/render.c +++ b/render.c @@ -2128,7 +2128,10 @@ render_search_box(struct terminal *term) * rendering etc. */ - const size_t total_cells = wcswidth(term->search.buf, term->search.len); + const wchar_t *text = term->search.buf; + const size_t text_len = term->search.len; + + const size_t total_cells = wcswidth(text, text_len); const size_t wanted_visible_cells = max(20, total_cells); assert(term->scale >= 1); @@ -2152,7 +2155,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( @@ -2178,7 +2181,7 @@ render_search_box(struct terminal *term) */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; - cell_idx += max(1, wcwidth(term->search.buf[i])), i++) + cell_idx += max(1, wcwidth(text[i])), i++) { if (i != term->search.cursor) continue; @@ -2203,12 +2206,12 @@ render_search_box(struct terminal *term) */ for (size_t i = 0, cell_idx = 0, - width = max(1, wcwidth(term->search.buf[i])), + width = max(1, wcwidth(text[i])), next_cell_idx = width; - i < term->search.len; + i < text_len; i++, cell_idx = next_cell_idx, - width = max(1, wcwidth(term->search.buf[i])), + width = max(1, wcwidth(text[i])), next_cell_idx += width) { if (i == term->search.cursor) @@ -2223,7 +2226,7 @@ render_search_box(struct terminal *term) } const struct fcft_glyph *glyph = fcft_glyph_rasterize( - font, term->search.buf[i], term->font_subpixel); + font, text[i], term->font_subpixel); if (glyph == NULL) { cell_idx = next_cell_idx; @@ -2249,7 +2252,7 @@ render_search_box(struct terminal *term) cell_idx = next_cell_idx; } - if (term->search.cursor >= term->search.len) + if (term->search.cursor >= text_len) draw_bar(term, buf->pix[0], font, &fg, x, y); quirk_weston_subsurface_desync_on(term->window->search_sub_surface); From 194fbff883e605a3a9afac4ecc66c6cf3cc6b7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 6 Dec 2020 12:17:52 +0100 Subject: [PATCH 37/46] ime: store wchar version of pre-edit string in terminal struct --- ime.c | 9 +++++---- terminal.c | 2 ++ terminal.h | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ime.c b/ime.c index 9c6676ab..d7dbc873 100644 --- a/ime.c +++ b/ime.c @@ -150,15 +150,16 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, if (wchars > 0) { /* First, convert to unicode */ - wchar_t wcs[wchars + 1]; - mbstowcs(wcs, seat->ime.preedit.pending.text, wchars); + 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(wcs[i]), 1); + int width = max(wcwidth(term->ime.preedit.text[i]), 1); widths[i] = width; cell_count += width; } @@ -174,7 +175,7 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, int width = widths[i]; - cell->wc = wcs[i]; + cell->wc = term->ime.preedit.text[i]; cell->attrs = (struct attributes){.clean = 0, .underline = 1}; for (int j = 1; j < width; j++) { diff --git a/terminal.c b/terminal.c index 215304ec..a626b88d 100644 --- a/terminal.c +++ b/terminal.c @@ -2819,7 +2819,9 @@ 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; } diff --git a/terminal.h b/terminal.h index f99c77c0..07723e1e 100644 --- a/terminal.h +++ b/terminal.h @@ -475,6 +475,7 @@ struct terminal { struct { bool enabled; struct { + wchar_t *text; struct cell *cells; int count; From fd42a0bc23aa0f19596f40449bf869f4245cc29c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 6 Dec 2020 12:18:17 +0100 Subject: [PATCH 38/46] ime: call render_refresh_search() instead of render_refresh() When scrollback search is active --- ime.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ime.c b/ime.c index d7dbc873..ccfa6310 100644 --- a/ime.c +++ b/ime.c @@ -117,7 +117,10 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, /* 1. Delete existing pre-edit text */ if (term->ime.preedit.cells != NULL) { term_ime_reset(term); - render_refresh(term); + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); } /* @@ -285,7 +288,10 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, term->ime.preedit.cursor.end = cell_end; } - render_refresh(term); + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); } ime_reset_preedit(seat); From d46dcd8ef54eba88dd76570e4b9fd2da284523e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 6 Dec 2020 12:18:46 +0100 Subject: [PATCH 39/46] render: ime: wip: pre-edit --- render.c | 97 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 75 insertions(+), 22 deletions(-) diff --git a/render.c b/render.c index 04f523cd..f5947eb6 100644 --- a/render.c +++ b/render.c @@ -2128,8 +2128,19 @@ render_search_box(struct terminal *term) * 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)); + wcscpy(text, term->search.buf); + if (term->ime.preedit.text != NULL) + wcscat(text, term->ime.preedit.text); +#else const wchar_t *text = term->search.buf; const size_t text_len = term->search.len; +#endif const size_t total_cells = wcswidth(text, text_len); const size_t wanted_visible_cells = max(20, total_cells); @@ -2168,7 +2179,8 @@ 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]); @@ -2179,6 +2191,8 @@ render_search_box(struct terminal *term) * position. Then we can ensure the cursor is within the rendered * part of the search string. */ + size_t cursor_cell_idx = 0; + for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += max(1, wcwidth(text[i])), i++) @@ -2186,12 +2200,21 @@ render_search_box(struct terminal *term) if (i != term->search.cursor) continue; +#if 0 +#if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime.preedit.cells != NULL) + cell_idx += term->ime.preedit.count; +#endif +#endif + if (cell_idx < glyph_offset) term->render.search_glyph_offset = glyph_offset = cell_idx; else if (cell_idx > glyph_offset + visible_cells) { term->render.search_glyph_offset = glyph_offset = cell_idx - min(cell_idx, visible_cells); } + assert(cell_idx >= glyph_offset); + cursor_cell_idx = cell_idx - glyph_offset; break; } @@ -2214,8 +2237,10 @@ render_search_box(struct terminal *term) width = max(1, wcwidth(text[i])), next_cell_idx += width) { +#if 0 if (i == term->search.cursor) draw_bar(term, buf->pix[0], font, &fg, x, y); +#endif if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) break; @@ -2252,8 +2277,36 @@ render_search_box(struct terminal *term) cell_idx = next_cell_idx; } - if (term->search.cursor >= text_len) - draw_bar(term, buf->pix[0], font, &fg, x, y); + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + + if (term->ime.preedit.cells != NULL) { + int cells_left = visible_cells - cursor_cell_idx; + int count = max(min(term->ime.preedit.count, cells_left), 0); + + /* Underline the entire pre-edit text */ + draw_underline(term, buf->pix[0], font, &fg, + x_left + cursor_cell_idx * term->cell_width, y, count); + + /* Cursor, unless hidden */ + if (!term->ime.preedit.cursor.hidden) { + /* TODO: we must ensure this is visible */ + const int start = cursor_cell_idx + term->ime.preedit.cursor.start; + const int end = cursor_cell_idx + term->ime.preedit.cursor.end; + + if (start == end) { + draw_bar(term, buf->pix[0], font, &fg, + x_left + start * term->cell_width, y); + } else { + draw_unfocused_block( + term, buf->pix[0], &fg, + x_left + start * term->cell_width, y, end - start); + } + } + } else +#endif + draw_bar(term, buf->pix[0], font, &fg, + x_left + cursor_cell_idx * term->cell_width, y); quirk_weston_subsurface_desync_on(term->window->search_sub_surface); @@ -2276,6 +2329,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 @@ -2648,32 +2705,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; From bcfc468fd0037ddeee64d3cf24602b570bacef3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 6 Dec 2020 12:24:45 +0100 Subject: [PATCH 40/46] render: ime: adjust cursor cell index when adjusting glyph offset --- render.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/render.c b/render.c index f5947eb6..9caadb09 100644 --- a/render.c +++ b/render.c @@ -2219,9 +2219,12 @@ render_search_box(struct terminal *term) } /* Move offset if there is free space available */ - if (total_cells - glyph_offset < visible_cells) + if (total_cells - glyph_offset < visible_cells) { + ssize_t old = glyph_offset; term->render.search_glyph_offset = glyph_offset = total_cells - min(total_cells, visible_cells); + cursor_cell_idx += old - (ssize_t)glyph_offset; + } /* * Render the search string, starting at ‘glyph_offset’. Note that From fc2bcf9bc0e1d01d191bd663d5493cfabff54420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Dec 2020 18:57:16 +0100 Subject: [PATCH 41/46] search: reset IME state when entering/exiting search mode --- search.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/search.c b/search.c index 83f86390..a7b6f112 100644 --- a/search.c +++ b/search.c @@ -99,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); } @@ -111,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; From a6ed9a9773bbd4081f741dc6d792086877cdcb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Dec 2020 18:57:49 +0100 Subject: [PATCH 42/46] =?UTF-8?q?ime:=20don=E2=80=99t=20underline=20charac?= =?UTF-8?q?ters=20inside=20the=20cursor-box?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ime.c | 256 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 131 insertions(+), 125 deletions(-) diff --git a/ime.c b/ime.c index ccfa6310..24237d69 100644 --- a/ime.c +++ b/ime.c @@ -151,150 +151,156 @@ done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, ? mbstowcs(NULL, seat->ime.preedit.pending.text, 0) : 0; - if (wchars > 0) { - /* 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'; + if (wchars == 0 || wchars == (size_t)-1) { + ime_reset_preedit(seat); + return; + } - /* Next, count number of cells needed */ - size_t cell_count = 0; - size_t widths[wchars + 1]; + /* 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'; - for (size_t i = 0; i < wchars; i++) { - int width = max(wcwidth(term->ime.preedit.text[i]), 1); - widths[i] = width; - cell_count += width; + /* 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}; } - /* Allocate cells */ - term->ime.preedit.cells = xmalloc( - cell_count * sizeof(term->ime.preedit.cells[0])); - term->ime.preedit.count = cell_count; + cell_idx += width; + } - /* Populate cells */ - for (size_t i = 0, cell_idx = 0; i < wchars; i++) { - struct cell *cell = &term->ime.preedit.cells[cell_idx]; + /* 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; + } - int width = widths[i]; + 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 + */ - cell->wc = term->ime.preedit.text[i]; - cell->attrs = (struct attributes){.clean = 0, .underline = 1}; + const size_t byte_len = strlen(seat->ime.preedit.pending.text); - 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) + 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++) { - /* 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; + 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; } - 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 - */ + if (seat->ime.preedit.pending.cursor_end >= byte_len) + cell_end = cell_count; - const size_t byte_len = strlen(seat->ime.preedit.pending.text); + /* 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); - 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; + if (cell_end < cell_begin) + cell_end = cell_begin; - /* 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[cell_idx].wc), 1); - wc_idx++; - } - - 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 1 - if (cell_end < cell_begin) - cell_end = cell_begin; -#else - if (cell_end <= cell_begin) { - cell_end = cell_begin + max( - wcwidth(term->ime.preedit.cells[cell_begin].wc), 1); - } -#endif - - /* 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; + /* 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++; } - if (term->is_searching) - render_refresh_search(term); - else - render_refresh(term); + 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 From b270f221d0328651fdac46cf081e2cbce4d74f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Dec 2020 18:58:01 +0100 Subject: [PATCH 43/46] render: search: render IME pre-edit text correctly, hopefully --- render.c | 179 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 117 insertions(+), 62 deletions(-) diff --git a/render.c b/render.c index 9caadb09..1c905990 100644 --- a/render.c +++ b/render.c @@ -2134,14 +2134,30 @@ render_search_box(struct terminal *term) text_len += wcslen(term->ime.preedit.text); wchar_t *text = xmalloc((text_len + 1) * sizeof(wchar_t)); - wcscpy(text, term->search.buf); + 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); @@ -2184,46 +2200,56 @@ render_search_box(struct terminal *term) int y = margin; pixman_color_t fg = color_hex_to_pixman(term->colors.table[0]); - /* - * Ensure cursor is visible - * - * First, we need to map the cursor character position to a cell - * position. Then we can ensure the cursor is within the rendered - * part of the search string. - */ - size_t cursor_cell_idx = 0; - - for (size_t i = 0, cell_idx = 0; - i <= term->search.cursor; - cell_idx += max(1, wcwidth(text[i])), i++) - { + /* 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; -#if 0 #if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED - if (term->ime.preedit.cells != NULL) - cell_idx += term->ime.preedit.count; -#endif + 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 - if (cell_idx < glyph_offset) + 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); } - assert(cell_idx >= glyph_offset); - cursor_cell_idx = cell_idx - glyph_offset; + + /* 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; } - /* Move offset if there is free space available */ - if (total_cells - glyph_offset < visible_cells) { - ssize_t old = glyph_offset; - term->render.search_glyph_offset = glyph_offset = - total_cells - min(total_cells, visible_cells); - cursor_cell_idx += old - (ssize_t)glyph_offset; + /* 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; + } } /* @@ -2232,23 +2258,75 @@ render_search_box(struct terminal *term) */ for (size_t i = 0, cell_idx = 0, - width = max(1, wcwidth(text[i])), + width = widths[i], next_cell_idx = width; i < text_len; i++, cell_idx = next_cell_idx, - width = max(1, wcwidth(text[i])), + width = widths[i], next_cell_idx += width) { -#if 0 - if (i == term->search.cursor) - draw_bar(term, buf->pix[0], font, &fg, x, y); -#endif + /* 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 (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) + 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; } @@ -2280,36 +2358,13 @@ render_search_box(struct terminal *term) cell_idx = next_cell_idx; } - #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - - if (term->ime.preedit.cells != NULL) { - int cells_left = visible_cells - cursor_cell_idx; - int count = max(min(term->ime.preedit.count, cells_left), 0); - - /* Underline the entire pre-edit text */ - draw_underline(term, buf->pix[0], font, &fg, - x_left + cursor_cell_idx * term->cell_width, y, count); - - /* Cursor, unless hidden */ - if (!term->ime.preedit.cursor.hidden) { - /* TODO: we must ensure this is visible */ - const int start = cursor_cell_idx + term->ime.preedit.cursor.start; - const int end = cursor_cell_idx + term->ime.preedit.cursor.end; - - if (start == end) { - draw_bar(term, buf->pix[0], font, &fg, - x_left + start * term->cell_width, y); - } else { - draw_unfocused_block( - term, buf->pix[0], &fg, - x_left + start * term->cell_width, y, end - start); - } - } - } else + if (term->ime.preedit.cells != NULL) + /* Already rendered */; + else #endif - draw_bar(term, buf->pix[0], font, &fg, - x_left + cursor_cell_idx * term->cell_width, y); + 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); From 2ac520b958c340e1fa60cecdfe70b2d147a2f465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Mon, 7 Dec 2020 20:40:36 +0100 Subject: [PATCH 44/46] changelog: DECSET 737769 - enable/disable IME --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb320d7..c8a2877f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,9 @@ means foot can be PGO:d in e.g. sandboxed build scripts. See * 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 From ae01caee8ce4678de945fc18e97b95feced5fde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 8 Dec 2020 19:17:46 +0100 Subject: [PATCH 45/46] render: ime: ensure cursor is visible --- render.c | 54 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/render.c b/render.c index 1c905990..df22dd6e 100644 --- a/render.c +++ b/render.c @@ -1046,6 +1046,7 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) 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); @@ -1054,6 +1055,30 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) 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); @@ -1079,27 +1104,26 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) 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]; + 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 = wcwidth(term->ime.preedit.cells[i].wc); - width = max(1, width); - + int width = max(1, wcwidth(cell->wc)); if (col_idx + i + width > term->cols) break; - row->cells[col_idx + i] = term->ime.preedit.cells[i]; + 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; - int end = term->ime.preedit.cursor.end; + 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]; + 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); @@ -1113,14 +1137,18 @@ render_ime_preedit(struct terminal *term, struct buffer *buf) if (end == start) { /* Bar */ - struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); - draw_bar(term, buf->pix[0], font, &cursor_color, x, y); + 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 */ - int cols = end - start; - draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + if (start >= 0 && end <= term->cols) { + int cols = end - start; + draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + } } } From 6494ba24c9a37cd792b5f2b9de7170edc4456b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Tue, 8 Dec 2020 19:19:09 +0100 Subject: [PATCH 46/46] install: simplify sentence --- INSTALL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALL.md b/INSTALL.md index c370347c..43655b81 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -133,7 +133,7 @@ mkdir -p bld/release && cd bld/release ### Options -All variants below support the following compile-time options: +Available compile-time options: | Option | Type | Default | Description | Extra dependencies | |----------|------|---------|---------------------|--------------------|