From 66d3f3f9c3fcd053acf4be4d7cfb8e858b91e674 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 3 Aug 2026 18:34:05 +0800 Subject: [PATCH] imgui: Clamp mouse y-coordinate in multi-line click/drag to text bounds (#15052) * imgui: Clamp mouse y-coordinate in multi-line click/drag to text bounds In single-line mode, click and drag already clamped y to the line's y-coordinate so the cursor would continue to follow the x-position when the mouse went off the top or bottom of the text. Multi-line mode did not clamp, so stb_text_locate_coord() would return 0 (above) or n (below), snapping the cursor to the very start or end of text and ignoring the x-coordinate entirely. Now both modes walk the row layout to compute the top of the first row (y_min) and bottom of the last row (y_max, minus half a line height to add tolerance for rounding), then clamp y to that range before passing it to stb_text_locate_coord(). This means dragging or clicking above the text now places the cursor on the first line at the x-coordinate, and dragging/clicking below places it on the last line at the x-coordinate, matching the single-line precedent. * Fix issue that cursor cannot be placed at the last empty line --- deps_src/imgui/imstb_textedit.h | 95 +++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/deps_src/imgui/imstb_textedit.h b/deps_src/imgui/imstb_textedit.h index 7644670975..3733bb2fa9 100644 --- a/deps_src/imgui/imstb_textedit.h +++ b/deps_src/imgui/imstb_textedit.h @@ -465,6 +465,57 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat STB_TEXTEDIT_LAYOUTROW(&r, str, 0); y = r.ymin; } + else + { + // In multi-line mode, clamp y to stay within the text vertical bounds. + // This lets the click still land at a valid location if the mouse is slightly + // above or below the text. + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + int i = 0; + float base_y = 0, y_min, y_max; + + // Get the first row to establish y_min and start the iteration + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + if (r.num_chars <= 0) + { + state->cursor = 0; + state->select_start = state->cursor; + state->select_end = state->cursor; + state->has_preferred_x = 0; + return; + } + y_min = r.ymin; + y_max = base_y + r.ymax; + i = r.num_chars; + base_y += r.baseline_y_delta; + + // Walk the remaining rows to find the bottom of the last row + while (i < n) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + break; + y_max = base_y + r.ymax; + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // If the text ends with a newline, account for the empty trailing line + // so the cursor can be placed on it + if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, n); + y_max = base_y + r.ymax; + } + + // Subtract half the last line height to avoid rounding issues when the mouse + // is just barely below the last line (keep cursor on the last line, not after the text) + y_max -= (r.ymax - r.ymin) * 0.5f; + + if (y < y_min) y = y_min; + if (y > y_max) y = y_max; + } state->cursor = stb_text_locate_coord(str, x, y); state->select_start = state->cursor; @@ -485,6 +536,50 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state STB_TEXTEDIT_LAYOUTROW(&r, str, 0); y = r.ymin; } + else + { + // In multi-line mode, clamp y to stay within the text vertical bounds. + // This lets the drag keep working if the mouse goes off the top or bottom of the text. + StbTexteditRow r; + int n = STB_TEXTEDIT_STRINGLEN(str); + int i = 0; + float base_y = 0, y_min, y_max; + + // Get the first row to establish y_min and start the iteration + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + if (r.num_chars <= 0) + return; + y_min = r.ymin; + y_max = base_y + r.ymax; + i = r.num_chars; + base_y += r.baseline_y_delta; + + // Walk the remaining rows to find the bottom of the last row + while (i < n) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, i); + if (r.num_chars <= 0) + break; + y_max = base_y + r.ymax; + i += r.num_chars; + base_y += r.baseline_y_delta; + } + + // If the text ends with a newline, account for the empty trailing line + // so the cursor can be placed on it + if (n > 0 && STB_TEXTEDIT_GETCHAR(str, n - 1) == STB_TEXTEDIT_NEWLINE) + { + STB_TEXTEDIT_LAYOUTROW(&r, str, n); + y_max = base_y + r.ymax; + } + + // Subtract half the last line height to avoid rounding issues when the mouse + // is just barely below the last line (keep cursor on the last line, not after the text) + y_max -= (r.ymax - r.ymin) * 0.5f; + + if (y < y_min) y = y_min; + if (y > y_max) y = y_max; + } if (state->select_start == state->select_end) state->select_start = state->cursor;