From 126aaddccb547fadbc4a87b11dea1982a806c0c6 Mon Sep 17 00:00:00 2001 From: pagedown Date: Wed, 22 Feb 2023 22:36:06 +0800 Subject: [PATCH 1/2] IME: Render overlay at the last visible cursor position with a separate cursor Fix the problem caused by wrong cursor coordinates. No more messing with the main cursor, instead the cursor is saved when receiving a pre-edit text update and used for drawing later. Update the overlay to the last visible cursor position before rendering to ensure it always moves with the cursor. Finally, draw the overlay after line rendering is complete, and restore the line buffer after updating the rendered data to ensure that the line text being read is correct at all times. This also improves performance by only rendering once when changes are made, eliminating the need to repeatedly disable and draw after various commands and not even comprehensively. --- docs/changelog.rst | 2 +- kitty/child-monitor.c | 12 +- kitty/keys.c | 15 +- kitty/screen.c | 312 ++++++++++++++++++++++++------------------ kitty/screen.h | 18 ++- kitty/shaders.c | 8 +- 6 files changed, 213 insertions(+), 154 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 463149449..28023d234 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -46,7 +46,7 @@ Detailed list of changes - When changing the cursor color via escape codes or remote control to a fixed color, do not reset cursor_text_color (:iss:`5994`) -- Input Method Extensions: Fix incorrect rendering of IME in-progress text in some situations (:pull:`6002`) +- Input Method Extensions: Fix incorrect rendering of IME in-progress and commited text in some situations (:pull:`6049`) - Linux: Reduce minimum required OpenGL version from 3.3 to 3.1 + extensions (:iss:`2790`) diff --git a/kitty/child-monitor.c b/kitty/child-monitor.c index 400703e52..e877083b3 100644 --- a/kitty/child-monitor.c +++ b/kitty/child-monitor.c @@ -626,8 +626,16 @@ cursor_needs_render(Window *w) { static bool collect_cursor_info(CursorRenderInfo *ans, Window *w, monotonic_t now, OSWindow *os_window) { ScreenRenderData *rd = &w->render_data; - Cursor *cursor = rd->screen->cursor; - ans->x = cursor->x; ans->y = cursor->y; + Cursor *cursor; + if (screen_is_overlay_active(rd->screen)) { + // Do not force the cursor to be visible here for the sake of some programs that prefer it hidden + cursor = &(rd->screen->overlay_line.original_line.cursor); + ans->x = rd->screen->overlay_line.cursor_x; + ans->y = rd->screen->overlay_line.ynum; + } else { + cursor = rd->screen->cursor; + ans->x = cursor->x; ans->y = cursor->y; + } ans->is_visible = false; if (rd->screen->scrolled_by || !screen_is_cursor_visible(rd->screen)) return cursor_needs_render(w); monotonic_t time_since_start_blink = now - os_window->cursor_blink_zero_time; diff --git a/kitty/keys.c b/kitty/keys.c index 2e12075a2..f433221c0 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -105,8 +105,13 @@ void update_ime_position(Window* w, Screen *screen) { unsigned int cell_width = global_state.callback_os_window->fonts_data->cell_width, cell_height = global_state.callback_os_window->fonts_data->cell_height; unsigned int left = w->geometry.left, top = w->geometry.top; - left += screen->cursor->x * cell_width; - top += screen->cursor->y * cell_height; + if (screen_is_overlay_active(screen)) { + left += screen->overlay_line.cursor_x * cell_width; + top += MIN(screen->overlay_line.ynum + screen->scrolled_by, screen->lines - 1) * cell_height; + } else { + left += screen->cursor->x * cell_width; + top += screen->cursor->y * cell_height; + } GLFWIMEUpdateEvent ev = { .type = GLFW_IME_UPDATE_CURSOR_POSITION }; ev.cursor.left = left; ev.cursor.top = top; ev.cursor.width = cell_width; ev.cursor.height = cell_height; glfwUpdateIMEState(global_state.callback_os_window->handle, &ev); @@ -154,12 +159,12 @@ on_key_input(GLFWkeyevent *ev) { case GLFW_IME_WAYLAND_DONE_EVENT: // If we update IME position here it sends GNOME's text input system into // an infinite loop. See https://github.com/kovidgoyal/kitty/issues/5105 - screen_draw_overlay_text(screen, NULL); + screen_update_overlay_text(screen, NULL); debug("handled wayland IME done event\n"); return; case GLFW_IME_PREEDIT_CHANGED: + screen_update_overlay_text(screen, text); update_ime_position(w, screen); - screen_draw_overlay_text(screen, text); debug("updated pre-edit text: '%s'\n", text); return; case GLFW_IME_COMMIT_TEXT: @@ -167,7 +172,7 @@ on_key_input(GLFWkeyevent *ev) { schedule_write_to_child(w->id, 1, text, strlen(text)); debug("committed pre-edit text: %s\n", text); } else debug("committed pre-edit text: (null)\n"); - screen_draw_overlay_text(screen, NULL); + screen_update_overlay_text(screen, NULL); return; case GLFW_IME_NONE: // for macOS, update ime position on every key input diff --git a/kitty/screen.c b/kitty/screen.c index 842396e31..85298c565 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -23,6 +23,7 @@ #include "unicode-data.h" #include "modes.h" #include "wcwidth-std.h" +#include "wcswidth.h" #include "control-codes.h" #include "charsets.h" #include "keys.h" @@ -49,25 +50,37 @@ init_tabstops(bool *tabstops, index_type count) { } static bool -init_overlay_line(Screen *self, index_type columns) { +init_overlay_line(Screen *self, index_type columns, bool keep_active) { PyMem_Free(self->overlay_line.cpu_cells); PyMem_Free(self->overlay_line.gpu_cells); + PyMem_Free(self->overlay_line.original_line.cpu_cells); + PyMem_Free(self->overlay_line.original_line.gpu_cells); self->overlay_line.cpu_cells = PyMem_Calloc(columns, sizeof(CPUCell)); self->overlay_line.gpu_cells = PyMem_Calloc(columns, sizeof(GPUCell)); - if (!self->overlay_line.cpu_cells || !self->overlay_line.gpu_cells) { + self->overlay_line.original_line.cpu_cells = PyMem_Calloc(columns, sizeof(CPUCell)); + self->overlay_line.original_line.gpu_cells = PyMem_Calloc(columns, sizeof(GPUCell)); + if (!self->overlay_line.cpu_cells || !self->overlay_line.gpu_cells || + !self->overlay_line.original_line.cpu_cells || !self->overlay_line.original_line.gpu_cells) { PyErr_NoMemory(); return false; } - self->overlay_line.is_active = false; - self->overlay_line.xnum = 0; + if (!keep_active) { + self->overlay_line.is_active = false; + self->overlay_line.xnum = 0; + } + self->overlay_line.is_dirty = true; self->overlay_line.ynum = 0; self->overlay_line.xstart = 0; + self->overlay_line.cursor_x = 0; + self->overlay_line.last_ime_pos.x = 0; + self->overlay_line.last_ime_pos.y = 0; + return true; } -static void save_overlay_line(Screen *self, const char* func_name); -static void restore_overlay_line(Screen *self); static void deactivate_overlay_line(Screen *self); -static void clear_saved_overlay_line(Screen *self); +static void update_overlay_position(Screen *self); +static void render_overlay_line(Screen *self, Line *line, FONTS_DATA_HANDLE fonts_data); +static void update_overlay_line_data(Screen *self, uint8_t *data); #define RESET_CHARSETS \ self->g0_charset = translation_table(0); \ @@ -144,8 +157,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) { init_tabstops(self->main_tabstops, self->columns); init_tabstops(self->alt_tabstops, self->columns); self->key_encoding_flags = self->main_key_encoding_flags; - if (!init_overlay_line(self, self->columns)) { Py_CLEAR(self); return NULL; } - clear_saved_overlay_line(self); + if (!init_overlay_line(self, self->columns, false)) { Py_CLEAR(self); return NULL; } self->hyperlink_pool = alloc_hyperlink_pool(); if (!self->hyperlink_pool) { Py_CLEAR(self); return PyErr_NoMemory(); } self->as_ansi_buf.hyperlink_pool = self->hyperlink_pool; @@ -158,8 +170,11 @@ static Line* range_line_(Screen *self, int y); void screen_reset(Screen *self) { if (self->linebuf == self->alt_linebuf) screen_toggle_screen_buffer(self, true, true); - if (self->overlay_line.is_active) deactivate_overlay_line(self); - clear_saved_overlay_line(self); + if (screen_is_overlay_active(self)) { + deactivate_overlay_line(self); + // Cancel IME composition + update_ime_position_for_window(self->window_id, false, -1); + } Py_CLEAR(self->last_reported_cwd); self->render_unfocused_cursor = false; memset(self->main_key_encoding_flags, 0, sizeof(self->main_key_encoding_flags)); @@ -336,7 +351,6 @@ found: static bool screen_resize(Screen *self, unsigned int lines, unsigned int columns) { - if (self->overlay_line.is_active) save_overlay_line(self, __func__); lines = MAX(1u, lines); columns = MAX(1u, columns); bool is_main = self->linebuf == self->main_linebuf; @@ -361,7 +375,7 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { which.num_content_lines = num_content_lines_after; \ } // Resize overlay line - if (!init_overlay_line(self, columns)) return false; + if (!init_overlay_line(self, columns, true)) return false; // Resize main linebuf HistoryBuf *nh = realloc_hb(self->historybuf, self->historybuf->ynum, columns, &self->as_ansi_buf); @@ -426,7 +440,6 @@ screen_resize(Screen *self, unsigned int lines, unsigned int columns) { self->linebuf->line->cpu_cells[0].ch = 0; self->cursor->x = 0; } - restore_overlay_line(self); return true; } @@ -463,7 +476,9 @@ dealloc(Screen* self) { Py_CLEAR(self->marker); PyMem_Free(self->overlay_line.cpu_cells); PyMem_Free(self->overlay_line.gpu_cells); - Py_CLEAR(self->overlay_line.save.overlay_text); + PyMem_Free(self->overlay_line.original_line.cpu_cells); + PyMem_Free(self->overlay_line.original_line.gpu_cells); + Py_CLEAR(self->overlay_line.overlay_text); PyMem_Free(self->main_tabstops); free(self->pending_mode.buf); free(self->selections.items); @@ -661,7 +676,6 @@ draw_combining_char(Screen *self, char_type ch) { } } - static void draw_codepoint(Screen *self, char_type och, bool from_input_stream) { if (is_ignored_char(och)) return; @@ -713,87 +727,11 @@ draw_codepoint(Screen *self, char_type och, bool from_input_stream) { linebuf_mark_line_dirty(self->linebuf, self->cursor->y); } -void -screen_draw_overlay_text(Screen *self, const char *utf8_text) { - if (self->overlay_line.is_active) deactivate_overlay_line(self); - if (self->overlay_line.save.overlay_text) clear_saved_overlay_line(self); - if (!utf8_text || !utf8_text[0]) return; - Line *line = range_line_(self, self->cursor->y); - if (!line) return; - line_save_cells(line, 0, self->columns, self->overlay_line.gpu_cells, self->overlay_line.cpu_cells); - self->overlay_line.is_active = true; - self->overlay_line.ynum = self->cursor->y; - self->overlay_line.xstart = self->cursor->x; - self->overlay_line.xnum = 0; - uint32_t codepoint = 0; UTF8State state = UTF8_ACCEPT; - bool orig_line_wrap_mode = self->modes.mDECAWM; - self->modes.mDECAWM = false; - self->cursor->reverse ^= true; - index_type before; - while (*utf8_text) { - switch(decode_utf8(&state, &codepoint, *(utf8_text++))) { - case UTF8_ACCEPT: - before = self->cursor->x; - draw_codepoint(self, codepoint, false); - self->overlay_line.xnum += self->cursor->x - before; - break; - case UTF8_REJECT: - break; - } - } - self->cursor->reverse ^= true; - self->modes.mDECAWM = orig_line_wrap_mode; -} - -static PyObject* -get_overlay_text(Screen *self) { -#define ol self->overlay_line - if (ol.ynum >= self->lines || ol.xnum >= self->columns || !ol.xnum) return NULL; - Line *line = range_line_(self, ol.ynum); - if (!line) return NULL; - return unicode_in_range(line, ol.xstart, ol.xstart + ol.xnum, true, false, true); -#undef ol -} - -static void -save_overlay_line(Screen *self, const char* func_name) { - if (self->overlay_line.is_active && screen_is_cursor_visible(self)) { - Py_XDECREF(self->overlay_line.save.overlay_text); - self->overlay_line.save.overlay_text = get_overlay_text(self); - self->overlay_line.save.func_name = func_name; - deactivate_overlay_line(self); - } -} - -static void -restore_overlay_line(Screen *self) { - if (self->overlay_line.save.overlay_text && screen_is_cursor_visible(self)) { - debug("Received input from child (%s) while overlay active. Overlay contents: %s\n", self->overlay_line.save.func_name, PyUnicode_AsUTF8(self->overlay_line.save.overlay_text)); - screen_draw_overlay_text(self, PyUnicode_AsUTF8(self->overlay_line.save.overlay_text)); - clear_saved_overlay_line(self); - update_ime_position_for_window(self->window_id, false, 0); - } -} - -static void -clear_saved_overlay_line(Screen *self) { - Py_CLEAR(self->overlay_line.save.overlay_text); -} - -static void -restore_overlay_line_from_cleanup(Screen **self) { - restore_overlay_line(*self); -} - -#define MOVE_OVERLAY_LINE_WITH_CURSOR Screen __attribute__ ((__cleanup__(restore_overlay_line_from_cleanup))) *_sol_ = self; save_overlay_line(_sol_, __func__); - void screen_draw(Screen *self, uint32_t och, bool from_input_stream) { - MOVE_OVERLAY_LINE_WITH_CURSOR; draw_codepoint(self, och, from_input_stream); } - void screen_align(Screen *self) { self->margin_top = 0; self->margin_bottom = self->lines - 1; @@ -819,7 +757,6 @@ screen_alignment_display(Screen *self) { void select_graphic_rendition(Screen *self, int *params, unsigned int count, Region *region_) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // needed in case colors have changed if (region_) { Region region = *region_; if (!region.top) region.top = 1; @@ -1018,14 +955,7 @@ set_mode_from_const(Screen *self, unsigned int mode, bool val) { self->modes.mDECCKM = val; break; case DECTCEM: - if(!val) { - save_overlay_line(self, __func__); - self->modes.mDECTCEM = val; - } else { - self->modes.mDECTCEM = val; - if (self->overlay_line.is_active && !self->overlay_line.save.overlay_text) save_overlay_line(self, __func__); - restore_overlay_line(self); - } + self->modes.mDECTCEM = val; break; case DECSCNM: // Render screen in reverse video @@ -1191,7 +1121,6 @@ screen_backspace(Screen *self) { void screen_tab(Screen *self) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Move to the next tab space, or the end of the screen if there aren't anymore left. unsigned int found = 0; for (unsigned int i = self->cursor->x + 1; i < self->columns; i++) { @@ -1223,7 +1152,6 @@ screen_tab(Screen *self) { void screen_backtab(Screen *self, unsigned int count) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Move back count tabs if (!count) count = 1; int i; @@ -1261,7 +1189,6 @@ screen_set_tab_stop(Screen *self) { void screen_cursor_back(Screen *self, unsigned int count/*=1*/, int move_direction/*=-1*/) { - MOVE_OVERLAY_LINE_WITH_CURSOR; if (count == 0) count = 1; if (move_direction < 0 && count > self->cursor->x) self->cursor->x = 0; else self->cursor->x += move_direction * count; @@ -1275,13 +1202,12 @@ screen_cursor_forward(Screen *self, unsigned int count/*=1*/) { void screen_cursor_up(Screen *self, unsigned int count/*=1*/, bool do_carriage_return/*=false*/, int move_direction/*=-1*/) { - MOVE_OVERLAY_LINE_WITH_CURSOR; bool in_margins = cursor_within_margins(self); if (count == 0) count = 1; if (move_direction < 0 && count > self->cursor->y) self->cursor->y = 0; else self->cursor->y += move_direction * count; - screen_ensure_bounds(self, true, in_margins); if (do_carriage_return) self->cursor->x = 0; + screen_ensure_bounds(self, true, in_margins); } void @@ -1301,7 +1227,6 @@ screen_cursor_down1(Screen *self, unsigned int count/*=1*/) { void screen_cursor_to_column(Screen *self, unsigned int column) { - MOVE_OVERLAY_LINE_WITH_CURSOR; unsigned int x = MAX(column, 1u) - 1; if (x != self->cursor->x) { self->cursor->x = x; @@ -1328,7 +1253,6 @@ screen_cursor_to_column(Screen *self, unsigned int column) { void screen_index(Screen *self) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Move cursor down one line, scrolling screen if needed unsigned int top = self->margin_top, bottom = self->margin_bottom; if (self->cursor->y == bottom) { @@ -1338,7 +1262,6 @@ screen_index(Screen *self) { void screen_scroll(Screen *self, unsigned int count) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Scroll the screen up by count lines, not moving the cursor unsigned int top = self->margin_top, bottom = self->margin_bottom; while (count > 0) { @@ -1349,7 +1272,6 @@ screen_scroll(Screen *self, unsigned int count) { void screen_reverse_index(Screen *self) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Move cursor up one line, scrolling screen if needed unsigned int top = self->margin_top, bottom = self->margin_bottom; if (self->cursor->y == top) { @@ -1359,7 +1281,6 @@ screen_reverse_index(Screen *self) { static void _reverse_scroll(Screen *self, unsigned int count, bool fill_from_scrollback) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Scroll the screen down by count lines, not moving the cursor unsigned int top = self->margin_top, bottom = self->margin_bottom; fill_from_scrollback = fill_from_scrollback && self->linebuf == self->main_linebuf; @@ -1389,7 +1310,6 @@ screen_reverse_scroll_and_fill_from_scrollback(Screen *self, unsigned int count) void screen_carriage_return(Screen *self) { if (self->cursor->x != 0) { - MOVE_OVERLAY_LINE_WITH_CURSOR; self->cursor->x = 0; } } @@ -1449,7 +1369,7 @@ copy_specific_mode(Screen *self, unsigned int mode, const ScreenModes *src, Scre SIMPLE_MODE(BRACKETED_PASTE) SIMPLE_MODE(FOCUS_TRACKING) SIMPLE_MODE(DECCKM) - SIDE_EFFECTS(DECTCEM) // side effect: redraw IME overlay line + SIMPLE_MODE(DECTCEM) SIMPLE_MODE(DECAWM) case MOUSE_BUTTON_TRACKING: case MOUSE_MOTION_TRACKING: case MOUSE_MOVE_TRACKING: dest->mouse_tracking_mode = src->mouse_tracking_mode; break; @@ -1504,7 +1424,6 @@ screen_save_modes(Screen *self) { void screen_restore_cursor(Screen *self) { - MOVE_OVERLAY_LINE_WITH_CURSOR; Savepoint *sp = self->linebuf == self->main_linebuf ? &self->main_savepoint : &self->alt_savepoint; if (!sp->is_valid) { screen_cursor_position(self, 1, 1); @@ -1542,7 +1461,6 @@ screen_ensure_bounds(Screen *self, bool force_use_margins/*=false*/, bool in_mar void screen_cursor_position(Screen *self, unsigned int line, unsigned int column) { - MOVE_OVERLAY_LINE_WITH_CURSOR; bool in_margins = cursor_within_margins(self); line = (line == 0 ? 1 : line) - 1; column = (column == 0 ? 1: column) - 1; @@ -1737,6 +1655,7 @@ screen_insert_lines(Screen *self, unsigned int count) { static void screen_scroll_until_cursor_prompt(Screen *self) { + bool in_margins = cursor_within_margins(self); int q = screen_cursor_at_a_shell_prompt(self); unsigned int y = q > -1 ? (unsigned int)q : self->cursor->y; unsigned int num_lines_to_scroll = MIN(self->margin_bottom, y); @@ -1744,6 +1663,7 @@ screen_scroll_until_cursor_prompt(Screen *self) { self->cursor->y = self->margin_bottom; while (num_lines_to_scroll--) screen_index(self); self->cursor->y = final_y; + screen_ensure_bounds(self, false, in_margins); } void @@ -1785,7 +1705,6 @@ screen_repeat_character(Screen *self, unsigned int count) { void screen_delete_characters(Screen *self, unsigned int count) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Delete characters, later characters are moved left const unsigned int bottom = self->lines ? self->lines - 1 : 0; if (count == 0) count = 1; @@ -1803,7 +1722,6 @@ screen_delete_characters(Screen *self, unsigned int count) { void screen_erase_characters(Screen *self, unsigned int count) { - MOVE_OVERLAY_LINE_WITH_CURSOR; // Delete characters replacing them by spaces if (count == 0) count = 1; unsigned int x = self->cursor->x; @@ -2280,11 +2198,13 @@ screen_has_marker(Screen *self) { void screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_data, bool cursor_has_moved) { + const bool is_overlay_active = screen_is_overlay_active(self); unsigned int history_line_added_count = self->history_line_added_count; index_type lnum; bool was_dirty = self->is_dirty; - if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count); screen_reset_dirty(self); + update_overlay_position(self); + if (self->scrolled_by) self->scrolled_by = MIN(self->scrolled_by + history_line_added_count, self->historybuf->count); self->scroll_changed = false; for (index_type y = 0; y < MIN(self->lines, self->scrolled_by); y++) { lnum = self->scrolled_by - 1 - y; @@ -2303,11 +2223,18 @@ screen_update_cell_data(Screen *self, void *address, FONTS_DATA_HANDLE fonts_dat (cursor_has_moved && (self->cursor->y == lnum || self->last_rendered.cursor_y == lnum))) { render_line(fonts_data, self->linebuf->line, lnum, self->cursor, self->disable_ligatures); if (self->linebuf->line->attrs.has_dirty_text && screen_has_marker(self)) mark_text_in_line(self->marker, self->linebuf->line); - + if (is_overlay_active && lnum == self->overlay_line.ynum) render_overlay_line(self, self->linebuf->line, fonts_data); linebuf_mark_line_clean(self->linebuf, lnum); } update_line_data(self->linebuf->line, y, address); } + if (is_overlay_active && self->overlay_line.ynum + self->scrolled_by < self->lines) { + if (self->overlay_line.is_dirty) { + linebuf_init_line(self->linebuf, self->overlay_line.ynum); + render_overlay_line(self, self->linebuf->line, fonts_data); + } + update_overlay_line_data(self, address); + } if (was_dirty) clear_selection(&self->url_ranges); } @@ -2696,22 +2623,6 @@ screen_open_url(Screen *self) { return found; } -static void -deactivate_overlay_line(Screen *self) { - if (self->overlay_line.is_active && self->overlay_line.xnum && self->overlay_line.ynum < self->lines) { - Line *line = range_line_(self, self->overlay_line.ynum); - line_reset_cells(line, self->overlay_line.xstart, self->overlay_line.xnum, self->overlay_line.gpu_cells, self->overlay_line.cpu_cells); - if (self->cursor->y == self->overlay_line.ynum) self->cursor->x = self->overlay_line.xstart; - self->is_dirty = true; - linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum); - } - self->overlay_line.is_active = false; - self->overlay_line.ynum = 0; - self->overlay_line.xnum = 0; - self->overlay_line.xstart = 0; -} - - // }}} // URLs {{{ @@ -2792,7 +2703,136 @@ screen_detect_url(Screen *screen, unsigned int x, unsigned int y) { return has_url ? -1 : 0; } +// }}} +// IME Overlay {{{ +bool +screen_is_overlay_active(Screen *self) { + return self->overlay_line.is_active; +} + +static void +deactivate_overlay_line(Screen *self) { + if (self->overlay_line.is_active && self->overlay_line.xnum && self->overlay_line.ynum < self->lines) { + self->is_dirty = true; + linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum); + } + self->overlay_line.is_active = false; + self->overlay_line.is_dirty = true; + self->overlay_line.ynum = 0; + self->overlay_line.xstart = 0; + self->overlay_line.cursor_x = 0; +} + +void +screen_update_overlay_text(Screen *self, const char *utf8_text) { + if (screen_is_overlay_active(self)) deactivate_overlay_line(self); + if (!utf8_text || !utf8_text[0]) return; + PyObject *text = PyUnicode_FromString(utf8_text); + if (!text) return; + Py_XDECREF(self->overlay_line.overlay_text); + // Calculate the total number of cells for initial overlay cursor position + PyObject *text_len = wcswidth_std(NULL, text); + self->overlay_line.overlay_text = text; + self->overlay_line.is_active = true; + self->overlay_line.is_dirty = true; + self->overlay_line.xstart = self->cursor->x; + self->overlay_line.xnum = !text_len ? 0 : PyLong_AsLong(text_len); + self->overlay_line.cursor_x = MIN(self->overlay_line.xstart + self->overlay_line.xnum, self->columns); + self->overlay_line.ynum = self->cursor->y; + cursor_copy_to(self->cursor, &(self->overlay_line.original_line.cursor)); + linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum); + self->is_dirty = true; + // Since we are typing, scroll to the bottom + if (self->scrolled_by != 0) { + self->scrolled_by = 0; + self->scroll_changed = true; + } +} + +static void +screen_draw_overlay_line(Screen *self) { + if (!self->overlay_line.overlay_text) return; + const char *utf8_text = PyUnicode_AsUTF8(self->overlay_line.overlay_text); + if (!utf8_text || !utf8_text[0]) return; + self->overlay_line.xnum = 0; + uint32_t codepoint = 0; UTF8State state = UTF8_ACCEPT; + bool orig_line_wrap_mode = self->modes.mDECAWM; + bool orig_cursor_enable_mode = self->modes.mDECTCEM; + bool orig_insert_replace_mode = self->modes.mIRM; + self->modes.mDECAWM = false; + self->modes.mDECTCEM = false; + self->modes.mIRM = false; + Cursor *orig_cursor = self->cursor; + self->cursor = &(self->overlay_line.original_line.cursor); + self->cursor->reverse ^= true; + self->cursor->x = self->overlay_line.xstart; + self->cursor->y = self->overlay_line.ynum; + self->overlay_line.xnum = 0; + index_type before; + while (*utf8_text) { + switch(decode_utf8(&state, &codepoint, *(utf8_text++))) { + case UTF8_ACCEPT: + before = self->cursor->x; + draw_codepoint(self, codepoint, false); + self->overlay_line.xnum += self->cursor->x - before; + break; + case UTF8_REJECT: + break; + } + } + self->overlay_line.cursor_x = self->cursor->x; + self->cursor->reverse ^= true; + self->cursor = orig_cursor; + self->modes.mDECAWM = orig_line_wrap_mode; + self->modes.mDECTCEM = orig_cursor_enable_mode; + self->modes.mIRM = orig_insert_replace_mode; +} + +static void +update_overlay_position(Screen *self) { + if (screen_is_overlay_active(self) && screen_is_cursor_visible(self)) { + bool cursor_update = false; + if (self->cursor->x != self->overlay_line.xstart) { + cursor_update = true; + self->overlay_line.xstart = self->cursor->x; + self->overlay_line.cursor_x = MIN(self->overlay_line.xstart + self->overlay_line.xnum, self->columns); + } + if (self->cursor->y != self->overlay_line.ynum) { + cursor_update = true; + linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum); + self->overlay_line.ynum = self->cursor->y; + } + if (cursor_update) { + linebuf_mark_line_dirty(self->linebuf, self->overlay_line.ynum); + self->overlay_line.is_dirty = true; + self->is_dirty = true; + } + } +} + +static void +render_overlay_line(Screen *self, Line *line, FONTS_DATA_HANDLE fonts_data) { +#define ol self->overlay_line + line_save_cells(line, 0, line->xnum, ol.original_line.gpu_cells, ol.original_line.cpu_cells); + screen_draw_overlay_line(self); + render_line(fonts_data, line, ol.ynum, self->cursor, self->disable_ligatures); + line_save_cells(line, 0, line->xnum, ol.gpu_cells, ol.cpu_cells); + line_reset_cells(line, 0, line->xnum, ol.original_line.gpu_cells, ol.original_line.cpu_cells); + ol.is_dirty = false; + const index_type y = MIN(ol.ynum + self->scrolled_by, self->lines - 1); + if (ol.last_ime_pos.x != ol.cursor_x || ol.last_ime_pos.y != y) { + ol.last_ime_pos.x = ol.cursor_x; ol.last_ime_pos.y = y; + update_ime_position_for_window(self->window_id, false, 0); + } +#undef ol +} + +static void +update_overlay_line_data(Screen *self, uint8_t *data) { + const size_t base = sizeof(GPUCell) * (self->overlay_line.ynum + self->scrolled_by) * self->columns; + memcpy(data + base, self->overlay_line.gpu_cells, self->columns * sizeof(GPUCell)); +} // }}} @@ -3921,7 +3961,7 @@ focus_changed(Screen *self, PyObject *has_focus_) { if (has_focus != previous) { self->has_focus = has_focus; if (has_focus) self->has_activity_since_last_focus = false; - else if (self->overlay_line.is_active) deactivate_overlay_line(self); + else if (screen_is_overlay_active(self)) deactivate_overlay_line(self); if (self->modes.mFOCUS_TRACKING) write_escape_code_to_child(self, CSI, has_focus ? "I" : "O"); Py_RETURN_TRUE; } diff --git a/kitty/screen.h b/kitty/screen.h index c14162090..ece3ed0a1 100644 --- a/kitty/screen.h +++ b/kitty/screen.h @@ -68,15 +68,20 @@ typedef struct { typedef struct { + PyObject *overlay_text; CPUCell *cpu_cells; GPUCell *gpu_cells; + index_type xstart, ynum, xnum, cursor_x; bool is_active; - index_type xstart, ynum, xnum; - + bool is_dirty; struct { - PyObject *overlay_text; - const char *func_name; - } save; + CPUCell *cpu_cells; + GPUCell *gpu_cells; + Cursor cursor; + } original_line; + struct { + index_type x, y; + } last_ime_pos; } OverlayLine; typedef struct { @@ -264,7 +269,8 @@ void screen_dirty_sprite_positions(Screen *self); void screen_rescale_images(Screen *self); void screen_report_size(Screen *, unsigned int which); void screen_manipulate_title_stack(Screen *, unsigned int op, unsigned int which); -void screen_draw_overlay_text(Screen *self, const char *utf8_text); +bool screen_is_overlay_active(Screen *self); +void screen_update_overlay_text(Screen *self, const char *utf8_text); void screen_set_key_encoding_flags(Screen *self, uint32_t val, uint32_t how); void screen_push_key_encoding_flags(Screen *self, uint32_t val); void screen_pop_key_encoding_flags(Screen *self, uint32_t num); diff --git a/kitty/shaders.c b/kitty/shaders.c index b4a1b7cbb..fb27a63c8 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -329,7 +329,7 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, c // Cursor position enum { BLOCK_IDX = 0, BEAM_IDX = NUM_UNDERLINE_STYLES + 3, UNDERLINE_IDX = NUM_UNDERLINE_STYLES + 4, UNFOCUSED_IDX = NUM_UNDERLINE_STYLES + 5 }; if (cursor->is_visible) { - rd->cursor_x = screen->cursor->x, rd->cursor_y = screen->cursor->y; + rd->cursor_x = cursor->x, rd->cursor_y = cursor->y; if (cursor->is_focused) { switch(cursor->shape) { default: @@ -341,11 +341,11 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, c } } else rd->cursor_fg_sprite_idx = UNFOCUSED_IDX; color_type cell_fg = rd->default_fg, cell_bg = rd->default_bg; - index_type cell_color_x = screen->cursor->x; - bool cursor_ok = screen->cursor->x < screen->columns && screen->cursor->y < screen->lines; + index_type cell_color_x = cursor->x; + bool cursor_ok = cursor->x < screen->columns && cursor->y < screen->lines; bool reversed = false; if (cursor_ok) { - linebuf_init_line(screen->linebuf, screen->cursor->y); + linebuf_init_line(screen->linebuf, cursor->y); colors_for_cell(screen->linebuf->line, screen->color_profile, &cell_color_x, &cell_fg, &cell_bg, &reversed); } if (IS_SPECIAL_COLOR(cursor_color)) { From 9a598237c6f654f59691d9277971cdb98dd01085 Mon Sep 17 00:00:00 2001 From: pagedown Date: Wed, 22 Feb 2023 22:36:20 +0800 Subject: [PATCH 2/2] macOS: Allow IME to actively get the cursor position in real time IME will automatically get the display position when needed, which keeps it consistent with the overlay as much as possible. Fix the issue that when IME is activated after mouse click, it is displayed at the wrong position. --- glfw/cocoa_window.m | 32 +++++++++++++++++--------------- glfw/glfw3.h | 2 ++ glfw/init.c | 7 +++++++ glfw/internal.h | 1 + kitty/glfw-wrapper.c | 3 +++ kitty/glfw-wrapper.h | 5 +++++ kitty/glfw.c | 21 +++++++++++++++++++++ kitty/keys.c | 13 ++++++++++--- 8 files changed, 66 insertions(+), 18 deletions(-) diff --git a/glfw/cocoa_window.m b/glfw/cocoa_window.m index 9b66956d4..bd0463589 100644 --- a/glfw/cocoa_window.m +++ b/glfw/cocoa_window.m @@ -1454,15 +1454,11 @@ is_ascii_control_char(char x) { } void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { - [w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused left:(CGFloat)ev->cursor.left top:(CGFloat)ev->cursor.top cellWidth:(CGFloat)ev->cursor.width cellHeight:(CGFloat)ev->cursor.height]; + [w->ns.view updateIMEStateFor: ev->type focused:(bool)ev->focused]; } - (void)updateIMEStateFor:(GLFWIMEUpdateType)which focused:(bool)focused - left:(CGFloat)left - top:(CGFloat)top - cellWidth:(CGFloat)cellWidth - cellHeight:(CGFloat)cellHeight { if (which == GLFW_IME_UPDATE_FOCUS && !focused && [self hasMarkedText] && window) { [input_context discardMarkedText]; @@ -1472,16 +1468,7 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { _glfw.ns.text[0] = 0; } if (which != GLFW_IME_UPDATE_CURSOR_POSITION) return; - left /= window->ns.xscale; - top /= window->ns.yscale; - cellWidth /= window->ns.xscale; - cellHeight /= window->ns.yscale; - debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight); - const NSRect frame = [window->ns.view frame]; - const NSRect rectInView = NSMakeRect(left, - frame.size.height - top - cellHeight, - cellWidth, cellHeight); - markedRect = [window->ns.object convertRectToScreen: rectInView]; + if (_glfwPlatformWindowFocused(window)) [[window->ns.view inputContext] invalidateCharacterCoordinates]; } @@ -1507,6 +1494,21 @@ void _glfwPlatformUpdateIMEState(_GLFWwindow *w, const GLFWIMEUpdateEvent *ev) { actualRange:(NSRangePointer)actualRange { (void)range; (void)actualRange; + if (_glfw.callbacks.get_ime_cursor_position) { + GLFWIMEUpdateEvent ev = { .type = GLFW_IME_UPDATE_CURSOR_POSITION }; + if (_glfw.callbacks.get_ime_cursor_position((GLFWwindow*)window, &ev)) { + const CGFloat left = (CGFloat)ev.cursor.left / window->ns.xscale; + const CGFloat top = (CGFloat)ev.cursor.top / window->ns.yscale; + const CGFloat cellWidth = (CGFloat)ev.cursor.width / window->ns.xscale; + const CGFloat cellHeight = (CGFloat)ev.cursor.height / window->ns.yscale; + debug_key("updateIMEPosition: left=%f, top=%f, width=%f, height=%f\n", left, top, cellWidth, cellHeight); + const NSRect frame = [window->ns.view frame]; + const NSRect rectInView = NSMakeRect(left, + frame.size.height - top - cellHeight, + cellWidth, cellHeight); + markedRect = [window->ns.object convertRectToScreen: rectInView]; + } + } return markedRect; } diff --git a/glfw/glfw3.h b/glfw/glfw3.h index 3d02e5448..35507f9b3 100644 --- a/glfw/glfw3.h +++ b/glfw/glfw3.h @@ -1732,6 +1732,7 @@ typedef enum { } GLFWClipboardType; typedef GLFWDataChunk (* GLFWclipboarditerfun)(const char *mime_type, void *iter, GLFWClipboardType ctype); typedef bool (* GLFWclipboardwritedatafun)(void *object, const char *data, size_t sz); +typedef bool (* GLFWimecursorpositionfun)(GLFWwindow *window, GLFWIMEUpdateEvent *ev); /*! @brief Video mode type. * @@ -1891,6 +1892,7 @@ GLFWAPI void glfwRemoveTimer(unsigned long long); GLFWAPI GLFWdrawtextfun glfwSetDrawTextFunction(GLFWdrawtextfun function); GLFWAPI GLFWcurrentselectionfun glfwSetCurrentSelectionCallback(GLFWcurrentselectionfun callback); GLFWAPI GLFWhascurrentselectionfun glfwSetHasCurrentSelectionCallback(GLFWhascurrentselectionfun callback); +GLFWAPI GLFWimecursorpositionfun glfwSetIMECursorPositionCallback(GLFWimecursorpositionfun callback); /*! @brief Terminates the GLFW library. * diff --git a/glfw/init.c b/glfw/init.c index d9fd00b75..d9fff1ae1 100644 --- a/glfw/init.c +++ b/glfw/init.c @@ -402,3 +402,10 @@ GLFWAPI GLFWhascurrentselectionfun glfwSetHasCurrentSelectionCallback(GLFWhascur _GLFW_SWAP_POINTERS(_glfw.callbacks.has_current_selection, cbfun); return cbfun; } + +GLFWAPI GLFWimecursorpositionfun glfwSetIMECursorPositionCallback(GLFWimecursorpositionfun cbfun) +{ + _GLFW_REQUIRE_INIT_OR_RETURN(NULL); + _GLFW_SWAP_POINTERS(_glfw.callbacks.get_ime_cursor_position, cbfun); + return cbfun; +} diff --git a/glfw/internal.h b/glfw/internal.h index fe4125a5f..f4044f6e7 100644 --- a/glfw/internal.h +++ b/glfw/internal.h @@ -635,6 +635,7 @@ struct _GLFWlibrary GLFWdrawtextfun draw_text; GLFWcurrentselectionfun get_current_selection; GLFWhascurrentselectionfun has_current_selection; + GLFWimecursorpositionfun get_ime_cursor_position; } callbacks; diff --git a/kitty/glfw-wrapper.c b/kitty/glfw-wrapper.c index 6ec499bd1..784615412 100644 --- a/kitty/glfw-wrapper.c +++ b/kitty/glfw-wrapper.c @@ -44,6 +44,9 @@ load_glfw(const char* path) { *(void **) (&glfwSetHasCurrentSelectionCallback_impl) = dlsym(handle, "glfwSetHasCurrentSelectionCallback"); if (glfwSetHasCurrentSelectionCallback_impl == NULL) fail("Failed to load glfw function glfwSetHasCurrentSelectionCallback with error: %s", dlerror()); + *(void **) (&glfwSetIMECursorPositionCallback_impl) = dlsym(handle, "glfwSetIMECursorPositionCallback"); + if (glfwSetIMECursorPositionCallback_impl == NULL) fail("Failed to load glfw function glfwSetIMECursorPositionCallback with error: %s", dlerror()); + *(void **) (&glfwTerminate_impl) = dlsym(handle, "glfwTerminate"); if (glfwTerminate_impl == NULL) fail("Failed to load glfw function glfwTerminate with error: %s", dlerror()); diff --git a/kitty/glfw-wrapper.h b/kitty/glfw-wrapper.h index d847480b2..501220d9b 100644 --- a/kitty/glfw-wrapper.h +++ b/kitty/glfw-wrapper.h @@ -1470,6 +1470,7 @@ typedef enum { } GLFWClipboardType; typedef GLFWDataChunk (* GLFWclipboarditerfun)(const char *mime_type, void *iter, GLFWClipboardType ctype); typedef bool (* GLFWclipboardwritedatafun)(void *object, const char *data, size_t sz); +typedef bool (* GLFWimecursorpositionfun)(GLFWwindow *window, GLFWIMEUpdateEvent *ev); /*! @brief Video mode type. * @@ -1667,6 +1668,10 @@ typedef GLFWhascurrentselectionfun (*glfwSetHasCurrentSelectionCallback_func)(GL GFW_EXTERN glfwSetHasCurrentSelectionCallback_func glfwSetHasCurrentSelectionCallback_impl; #define glfwSetHasCurrentSelectionCallback glfwSetHasCurrentSelectionCallback_impl +typedef GLFWimecursorpositionfun (*glfwSetIMECursorPositionCallback_func)(GLFWimecursorpositionfun); +GFW_EXTERN glfwSetIMECursorPositionCallback_func glfwSetIMECursorPositionCallback_impl; +#define glfwSetIMECursorPositionCallback glfwSetIMECursorPositionCallback_impl + typedef void (*glfwTerminate_func)(void); GFW_EXTERN glfwTerminate_func glfwTerminate_impl; #define glfwTerminate glfwTerminate_impl diff --git a/kitty/glfw.c b/kitty/glfw.c index e72a87c35..784a3f347 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -507,6 +507,26 @@ has_current_selection(void) { return ans; } +void prepare_ime_position_update_event(OSWindow *osw, Window *w, Screen *screen, GLFWIMEUpdateEvent *ev); + +static bool +get_ime_cursor_position(GLFWwindow *glfw_window, GLFWIMEUpdateEvent *ev) { + if (!set_callback_window(glfw_window)) return false; + bool ans = false; + OSWindow *osw = global_state.callback_os_window; + if (osw && osw->is_focused && is_window_ready_for_callbacks()) { + Tab *tab = osw->tabs + osw->active_tab; + Window *w = tab->windows + tab->active_window; + Screen *screen = w->render_data.screen; + if (screen) { + prepare_ime_position_update_event(osw, w, screen, ev); + ans = true; + } + } + global_state.callback_os_window = NULL; + return ans; +} + static void get_window_dpi(GLFWwindow *w, double *x, double *y); @@ -832,6 +852,7 @@ create_os_window(PyObject UNUSED *self, PyObject *args, PyObject *kw) { glfwSetApplicationCloseCallback(application_close_requested_callback); glfwSetCurrentSelectionCallback(get_current_selection); glfwSetHasCurrentSelectionCallback(has_current_selection); + glfwSetIMECursorPositionCallback(get_ime_cursor_position); #ifdef __APPLE__ cocoa_set_activation_policy(OPT(macos_hide_from_tasks)); glfwWindowHint(GLFW_COCOA_GRAPHICS_SWITCHING, true); diff --git a/kitty/keys.c b/kitty/keys.c index f433221c0..24f4a714d 100644 --- a/kitty/keys.c +++ b/kitty/keys.c @@ -102,8 +102,8 @@ update_ime_focus(OSWindow *osw, bool focused) { } void -update_ime_position(Window* w, Screen *screen) { - unsigned int cell_width = global_state.callback_os_window->fonts_data->cell_width, cell_height = global_state.callback_os_window->fonts_data->cell_height; +prepare_ime_position_update_event(OSWindow *osw, Window *w, Screen *screen, GLFWIMEUpdateEvent *ev) { + unsigned int cell_width = osw->fonts_data->cell_width, cell_height = osw->fonts_data->cell_height; unsigned int left = w->geometry.left, top = w->geometry.top; if (screen_is_overlay_active(screen)) { left += screen->overlay_line.cursor_x * cell_width; @@ -112,8 +112,15 @@ update_ime_position(Window* w, Screen *screen) { left += screen->cursor->x * cell_width; top += screen->cursor->y * cell_height; } + ev->cursor.left = left; ev->cursor.top = top; ev->cursor.width = cell_width; ev->cursor.height = cell_height; +} + +void +update_ime_position(Window* w UNUSED, Screen *screen UNUSED) { GLFWIMEUpdateEvent ev = { .type = GLFW_IME_UPDATE_CURSOR_POSITION }; - ev.cursor.left = left; ev.cursor.top = top; ev.cursor.width = cell_width; ev.cursor.height = cell_height; +#ifndef __APPLE__ + prepare_ime_position_update_event(global_state.callback_os_window, w, screen, &ev); +#endif glfwUpdateIMEState(global_state.callback_os_window->handle, &ev); }