From 37472c71ec978325739f9eeb3218d13e620a967e Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 4 Dec 2025 15:18:02 -0800 Subject: [PATCH] Fix UI cursor positioning issues. Accurately recompute cursor position to prevent drift in terminal and GUI renderers. --- GUIRenderer.cc | 14 +++++++++++++- TerminalRenderer.cc | 28 ++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/GUIRenderer.cc b/GUIRenderer.cc index fd54814..2d8fa91 100644 --- a/GUIRenderer.cc +++ b/GUIRenderer.cc @@ -455,7 +455,19 @@ GUIRenderer::Draw(Editor &ed) } // Convert to viewport x by subtracting horizontal col offset std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0; - ImVec2 p0 = ImVec2(line_pos.x + static_cast(rx_viewport) * space_w, line_pos.y); + // For proportional fonts (Linux GUI), avoid accumulating drift by computing + // the exact pixel width of the expanded substring up to the cursor. + // expanded contains the line with tabs expanded to spaces and is what we draw. + float cursor_px = 0.0f; + if (rx_viewport > 0 && coloffs_now < expanded.size()) { + std::size_t start = coloffs_now; + std::size_t end = std::min(expanded.size(), start + rx_viewport); + // Measure substring width in pixels + ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start, + expanded.c_str() + end); + cursor_px = sz.x; + } + ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y); ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h); ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc index 0dee769..52d7c08 100644 --- a/TerminalRenderer.cc +++ b/TerminalRenderer.cc @@ -294,11 +294,31 @@ TerminalRenderer::Draw(Editor &ed) clrtoeol(); } - // Place terminal cursor at logical position accounting for tabs and coloffs + // Place terminal cursor at logical position accounting for tabs and coloffs. + // Recompute the rendered X using the same logic as the drawing loop to avoid + // any drift between the command-layer computation and the terminal renderer. std::size_t cy = buf->Cury(); - std::size_t rx = buf->Rx(); // render x computed by command layer - int cur_y = static_cast(cy) - static_cast(buf->Rowoffs()); - int cur_x = static_cast(rx) - static_cast(buf->Coloffs()); + std::size_t cx = buf->Curx(); + int cur_y = static_cast(cy) - static_cast(buf->Rowoffs()); + std::size_t rx_recomputed = 0; + if (cy < lines.size()) { + const std::string line_for_cursor = static_cast(lines[cy]); + std::size_t src_i_cur = 0; + std::size_t render_col_cur = 0; + while (src_i_cur < line_for_cursor.size() && src_i_cur < cx) { + unsigned char ccur = static_cast(line_for_cursor[src_i_cur]); + if (ccur == '\t') { + std::size_t next_tab = tabw - (render_col_cur % tabw); + render_col_cur += next_tab; + ++src_i_cur; + } else { + ++render_col_cur; + ++src_i_cur; + } + } + rx_recomputed = render_col_cur; + } + int cur_x = static_cast(rx_recomputed) - static_cast(buf->Coloffs()); if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) { // remember where to leave the terminal cursor after status is drawn saved_cur_y = cur_y;