From 83860ac13d7dcb300229168c2bb885f4eed9cc6d Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 18 Mar 2026 00:17:25 -0500 Subject: [PATCH] More text editing code consolidated. --- dvx/widgets/widgetComboBox.c | 85 +--------- dvx/widgets/widgetInternal.h | 3 + dvx/widgets/widgetSpinner.c | 77 +-------- dvx/widgets/widgetTextInput.c | 288 ++++++++++++++++++---------------- 4 files changed, 165 insertions(+), 288 deletions(-) diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index ee7c935..35bca24 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -291,46 +291,7 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { clearOtherSelections(w); AppContextT *ctx = (AppContextT *)root->userData; - const BitmapFontT *font = &ctx->font; - int32_t relX = vx - w->x - TEXT_INPUT_PAD; - int32_t charPos = relX / font->charWidth + w->as.comboBox.scrollOff; - - if (charPos < 0) { - charPos = 0; - } - - if (charPos > w->as.comboBox.len) { - charPos = w->as.comboBox.len; - } - - int32_t clicks = multiClickDetect(vx, vy); - - if (clicks >= 3) { - // Triple-click: select all (single line) - w->as.comboBox.selStart = 0; - w->as.comboBox.selEnd = w->as.comboBox.len; - w->as.comboBox.cursorPos = w->as.comboBox.len; - sDragTextSelect = NULL; - return; - } - - if (clicks == 2 && w->as.comboBox.buf) { - // Double-click: select word - int32_t ws = wordStart(w->as.comboBox.buf, charPos); - int32_t we = wordEnd(w->as.comboBox.buf, w->as.comboBox.len, charPos); - - w->as.comboBox.selStart = ws; - w->as.comboBox.selEnd = we; - w->as.comboBox.cursorPos = we; - sDragTextSelect = NULL; - return; - } - - // Single click: place cursor + start drag-select - w->as.comboBox.cursorPos = charPos; - w->as.comboBox.selStart = charPos; - w->as.comboBox.selEnd = charPos; - sDragTextSelect = w; + widgetTextEditMouseClick(w, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, w->as.comboBox.buf, w->as.comboBox.len, w->as.comboBox.scrollOff, &w->as.comboBox.cursorPos, &w->as.comboBox.selStart, &w->as.comboBox.selEnd, true, true); } } @@ -370,49 +331,7 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit len = maxChars; } - // Selection range - int32_t selLo = -1; - int32_t selHi = -1; - - if (w->as.comboBox.selStart >= 0 && w->as.comboBox.selEnd >= 0 && w->as.comboBox.selStart != w->as.comboBox.selEnd) { - selLo = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selStart : w->as.comboBox.selEnd; - selHi = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selEnd : w->as.comboBox.selStart; - } - - // Draw up to 3 text runs: before selection (normal colors), selection - // (highlight colors), after selection (normal colors). This avoids - // drawing the entire line twice (once normal, once selected) which - // would be wasteful on slow hardware. The visible selection range - // is clamped to the scrolled viewport. - int32_t visSelLo = selLo - off; - int32_t visSelHi = selHi - off; - - if (visSelLo < 0) { visSelLo = 0; } - if (visSelHi > len) { visSelHi = len; } - - if (selLo >= 0 && visSelLo < visSelHi) { - if (visSelLo > 0) { - drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, visSelLo, fg, bg, true); - } - - drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, w->as.comboBox.buf + off + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); - - if (visSelHi < len) { - drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, w->as.comboBox.buf + off + visSelHi, len - visSelHi, fg, bg, true); - } - } else { - drawTextN(d, ops, font, textX, textY, w->as.comboBox.buf + off, len, fg, bg, true); - } - - // Draw cursor (blinks at same rate as terminal cursor) - if (w->focused && w->enabled && !w->as.comboBox.open && sCursorBlinkOn) { - int32_t cursorX = textX + (w->as.comboBox.cursorPos - off) * font->charWidth; - - if (cursorX >= w->x + TEXT_INPUT_PAD && - cursorX < w->x + textAreaW - TEXT_INPUT_PAD) { - drawVLine(d, ops, cursorX, textY, font->charHeight, fg); - } - } + widgetTextEditPaintLine(d, ops, font, colors, textX, textY, w->as.comboBox.buf + off, len, off, w->as.comboBox.cursorPos, w->as.comboBox.selStart, w->as.comboBox.selEnd, fg, bg, w->focused && w->enabled && !w->as.comboBox.open, w->x + TEXT_INPUT_PAD, w->x + textAreaW - TEXT_INPUT_PAD); } // Drop button diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 687f0d7..477a876 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -477,7 +477,10 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); // (which store these fields in different union members). This avoids // duplicating the full editing logic (cursor movement, word selection, // clipboard, undo) three times. +void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd); +void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect); void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); +void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX); void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); int32_t wordEnd(const char *buf, int32_t len, int32_t pos); diff --git a/dvx/widgets/widgetSpinner.c b/dvx/widgets/widgetSpinner.c index e169b60..564f152 100644 --- a/dvx/widgets/widgetSpinner.c +++ b/dvx/widgets/widgetSpinner.c @@ -305,34 +305,8 @@ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { } } else { // Click on text area - AppContextT *ctx = (AppContextT *)root->userData; - const BitmapFontT *font = &ctx->font; - int32_t textX = hit->x + SPINNER_BORDER + SPINNER_PAD; - int32_t relX = vx - textX + hit->as.spinner.scrollOff * font->charWidth; - int32_t pos = relX / font->charWidth; - - if (pos < 0) { - pos = 0; - } - - if (pos > hit->as.spinner.len) { - pos = hit->as.spinner.len; - } - - int32_t clicks = multiClickDetect(vx, vy); - - if (clicks >= 2) { - // Double/triple-click: select all text - hit->as.spinner.selStart = 0; - hit->as.spinner.selEnd = hit->as.spinner.len; - hit->as.spinner.cursorPos = hit->as.spinner.len; - } else { - // Single click: place cursor - hit->as.spinner.cursorPos = pos; - hit->as.spinner.selStart = -1; - hit->as.spinner.selEnd = -1; - } - + AppContextT *ctx = (AppContextT *)root->userData; + widgetTextEditMouseClick(hit, vx, vy, hit->x + SPINNER_BORDER + SPINNER_PAD, &ctx->font, hit->as.spinner.buf, hit->as.spinner.len, hit->as.spinner.scrollOff, &hit->as.spinner.cursorPos, &hit->as.spinner.selStart, &hit->as.spinner.selEnd, false, false); spinnerStartEdit(hit); } @@ -398,52 +372,7 @@ void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm len = 0; } - // Selection range - int32_t selLo = -1; - int32_t selHi = -1; - - if (w->as.spinner.selStart >= 0 && w->as.spinner.selEnd >= 0 && w->as.spinner.selStart != w->as.spinner.selEnd) { - selLo = w->as.spinner.selStart < w->as.spinner.selEnd ? w->as.spinner.selStart : w->as.spinner.selEnd; - selHi = w->as.spinner.selStart > w->as.spinner.selEnd ? w->as.spinner.selStart : w->as.spinner.selEnd; - } - - // Draw text with selection highlighting (3-run approach like textInput) - int32_t visSelLo = selLo - off; - int32_t visSelHi = selHi - off; - - if (visSelLo < 0) { - visSelLo = 0; - } - - if (visSelHi > len) { - visSelHi = len; - } - - if (selLo >= 0 && visSelLo < visSelHi) { - // Before selection - if (visSelLo > 0) { - drawTextN(d, ops, font, textX, textY, &w->as.spinner.buf[off], visSelLo, fg, bg, true); - } - - // Selection - drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, &w->as.spinner.buf[off + visSelLo], visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); - - // After selection - if (visSelHi < len) { - drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, &w->as.spinner.buf[off + visSelHi], len - visSelHi, fg, bg, true); - } - } else if (len > 0) { - drawTextN(d, ops, font, textX, textY, &w->as.spinner.buf[off], len, fg, bg, true); - } - - // Cursor (blinks at same rate as terminal cursor) - if (w->focused && sCursorBlinkOn) { - int32_t curX = textX + (w->as.spinner.cursorPos - off) * font->charWidth; - - if (curX >= w->x + SPINNER_BORDER && curX < btnX - SPINNER_PAD) { - drawVLine(d, ops, curX, textY, font->charHeight, fg); - } - } + widgetTextEditPaintLine(d, ops, font, colors, textX, textY, &w->as.spinner.buf[off], len, off, w->as.spinner.cursorPos, w->as.spinner.selStart, w->as.spinner.selEnd, fg, bg, w->focused, w->x + SPINNER_BORDER, btnX - SPINNER_PAD); // Up button (top half) int32_t btnTopH = w->h / 2; diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index ff9d74f..1ca278f 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -160,6 +160,155 @@ void wgtUpdateCursorBlink(void) { } +// ============================================================ +// Shared single-line text editing: mouse click +// ============================================================ +// +// Computes cursor position from pixel coordinates, handles multi-click +// (double = word select, triple = select all), and optionally starts +// drag-select. Used by TextInput, ComboBox, and Spinner mouse handlers +// so click-to-cursor behavior is consistent across all text widgets. +// +// wordSelect: if true, double-click selects the word under the cursor; +// if false, double-click selects all (used by Spinner). +// dragSelect: if true, single-click registers the widget for drag-select +// tracking (TextInput/ComboBox); false for Spinner. + +void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect) { + int32_t relX = vx - textLeftX; + int32_t charPos = relX / font->charWidth + scrollOff; + + if (charPos < 0) { + charPos = 0; + } + + if (charPos > len) { + charPos = len; + } + + int32_t clicks = multiClickDetect(vx, vy); + + if (clicks >= 3) { + *pSelStart = 0; + *pSelEnd = len; + *pCursorPos = len; + sDragTextSelect = NULL; + return; + } + + if (clicks == 2) { + if (wordSelect && buf) { + int32_t ws = wordStart(buf, charPos); + int32_t we = wordEnd(buf, len, charPos); + *pSelStart = ws; + *pSelEnd = we; + *pCursorPos = we; + } else { + *pSelStart = 0; + *pSelEnd = len; + *pCursorPos = len; + } + + sDragTextSelect = NULL; + return; + } + + // Single click: place cursor + *pCursorPos = charPos; + *pSelStart = charPos; + *pSelEnd = charPos; + sDragTextSelect = dragSelect ? w : NULL; +} + + +// ============================================================ +// Shared single-line text editing: drag update +// ============================================================ +// +// Called during mouse drag to extend the selection. Auto-scrolls +// when the mouse moves past the visible text edges. Used by +// widgetTextDragUpdate for TextInput and ComboBox. + +void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd) { + int32_t rightEdge = leftEdge + maxChars * font->charWidth; + + if (vx < leftEdge && *pScrollOff > 0) { + (*pScrollOff)--; + } else if (vx >= rightEdge && *pScrollOff + maxChars < len) { + (*pScrollOff)++; + } + + int32_t relX = vx - leftEdge; + int32_t charPos = relX / font->charWidth + *pScrollOff; + + if (charPos < 0) { + charPos = 0; + } + + if (charPos > len) { + charPos = len; + } + + *pCursorPos = charPos; + *pSelEnd = charPos; +} + + +// ============================================================ +// Shared single-line text editing: paint +// ============================================================ +// +// Renders a single line of text with optional selection highlighting +// and a blinking cursor. Draws up to 3 runs (before/during/after +// selection) to avoid overdraw. Used by TextInput, ComboBox, and +// Spinner paint functions so selection rendering is identical. +// +// buf points to the already-scrolled display text (buf + scrollOff), +// and visLen is the number of visible characters. For password mode, +// the caller passes a bullet-filled display buffer. + +void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX) { + // Normalize selection to low/high + int32_t selLo = -1; + int32_t selHi = -1; + + if (selStart >= 0 && selEnd >= 0 && selStart != selEnd) { + selLo = selStart < selEnd ? selStart : selEnd; + selHi = selStart < selEnd ? selEnd : selStart; + } + + // Map selection to visible range + int32_t visSelLo = selLo - scrollOff; + int32_t visSelHi = selHi - scrollOff; + + if (visSelLo < 0) { visSelLo = 0; } + if (visSelHi > visLen) { visSelHi = visLen; } + + if (selLo >= 0 && visSelLo < visSelHi) { + if (visSelLo > 0) { + drawTextN(d, ops, font, textX, textY, buf, visSelLo, fg, bg, true); + } + + drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, buf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); + + if (visSelHi < visLen) { + drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, buf + visSelHi, visLen - visSelHi, fg, bg, true); + } + } else if (visLen > 0) { + drawTextN(d, ops, font, textX, textY, buf, visLen, fg, bg, true); + } + + // Blinking cursor + if (showCursor && sCursorBlinkOn) { + int32_t cursorX = textX + (cursorPos - scrollOff) * font->charWidth; + + if (cursorX >= cursorMinX && cursorX < cursorMaxX) { + drawVLine(d, ops, cursorX, textY, font->charHeight, fg); + } + } +} + + // ============================================================ // Multi-click tracking // ============================================================ @@ -1830,30 +1979,9 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { const BitmapFontT *font = &ctx->font; if (w->type == WidgetTextInputE) { - int32_t leftEdge = w->x + TEXT_INPUT_PAD; - int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; - int32_t rightEdge = leftEdge + maxChars * font->charWidth; - - // Auto-scroll when mouse is past edges - if (vx < leftEdge && w->as.textInput.scrollOff > 0) { - w->as.textInput.scrollOff--; - } else if (vx >= rightEdge && w->as.textInput.scrollOff + maxChars < w->as.textInput.len) { - w->as.textInput.scrollOff++; - } - - int32_t relX = vx - leftEdge; - int32_t charPos = relX / font->charWidth + w->as.textInput.scrollOff; - - if (charPos < 0) { - charPos = 0; - } - - if (charPos > w->as.textInput.len) { - charPos = w->as.textInput.len; - } - - w->as.textInput.cursorPos = charPos; - w->as.textInput.selEnd = charPos; + int32_t leftEdge = w->x + TEXT_INPUT_PAD; + int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; + widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.textInput.len, &w->as.textInput.cursorPos, &w->as.textInput.scrollOff, &w->as.textInput.selEnd); } else if (w->type == WidgetTextAreaE) { int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t innerY = w->y + TEXTAREA_BORDER; @@ -1918,30 +2046,9 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { w->as.textArea.desiredCol = clickCol; w->as.textArea.selCursor = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); } else if (w->type == WidgetComboBoxE) { - int32_t leftEdge = w->x + TEXT_INPUT_PAD; - int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth; - int32_t rightEdge = leftEdge + maxChars * font->charWidth; - - // Auto-scroll when mouse is past edges - if (vx < leftEdge && w->as.comboBox.scrollOff > 0) { - w->as.comboBox.scrollOff--; - } else if (vx >= rightEdge && w->as.comboBox.scrollOff + maxChars < w->as.comboBox.len) { - w->as.comboBox.scrollOff++; - } - - int32_t relX = vx - leftEdge; - int32_t charPos = relX / font->charWidth + w->as.comboBox.scrollOff; - - if (charPos < 0) { - charPos = 0; - } - - if (charPos > w->as.comboBox.len) { - charPos = w->as.comboBox.len; - } - - w->as.comboBox.cursorPos = charPos; - w->as.comboBox.selEnd = charPos; + int32_t leftEdge = w->x + TEXT_INPUT_PAD; + int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth; + widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.comboBox.len, &w->as.comboBox.cursorPos, &w->as.comboBox.scrollOff, &w->as.comboBox.selEnd); } else if (w->type == WidgetAnsiTermE) { int32_t baseX = w->x + 2; // ANSI_BORDER int32_t baseY = w->y + 2; @@ -2360,46 +2467,7 @@ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { clearOtherSelections(w); AppContextT *ctx = (AppContextT *)root->userData; - const BitmapFontT *font = &ctx->font; - int32_t relX = vx - w->x - TEXT_INPUT_PAD; - int32_t charPos = relX / font->charWidth + w->as.textInput.scrollOff; - - if (charPos < 0) { - charPos = 0; - } - - if (charPos > w->as.textInput.len) { - charPos = w->as.textInput.len; - } - - int32_t clicks = multiClickDetect(vx, vy); - - if (clicks >= 3) { - // Triple-click: select all (single line) - w->as.textInput.selStart = 0; - w->as.textInput.selEnd = w->as.textInput.len; - w->as.textInput.cursorPos = w->as.textInput.len; - sDragTextSelect = NULL; - return; - } - - if (clicks == 2 && w->as.textInput.buf) { - // Double-click: select word - int32_t ws = wordStart(w->as.textInput.buf, charPos); - int32_t we = wordEnd(w->as.textInput.buf, w->as.textInput.len, charPos); - - w->as.textInput.selStart = ws; - w->as.textInput.selEnd = we; - w->as.textInput.cursorPos = we; - sDragTextSelect = NULL; - return; - } - - // Single click: place cursor + start drag-select - w->as.textInput.cursorPos = charPos; - w->as.textInput.selStart = charPos; - w->as.textInput.selEnd = charPos; - sDragTextSelect = w; + widgetTextEditMouseClick(w, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, w->as.textInput.buf, w->as.textInput.len, w->as.textInput.scrollOff, &w->as.textInput.cursorPos, &w->as.textInput.selStart, &w->as.textInput.selEnd, true, true); } @@ -2440,15 +2508,6 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi len = maxChars; } - // Selection range - int32_t selLo = -1; - int32_t selHi = -1; - - if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) { - selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd; - selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart; - } - bool isPassword = (w->as.textInput.inputMode == InputPasswordE); // Build display buffer (password masking) @@ -2461,40 +2520,7 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi memcpy(dispBuf, w->as.textInput.buf + off, dispLen); } - // Draw up to 3 runs: before selection, selection, after selection - int32_t visSelLo = selLo - off; - int32_t visSelHi = selHi - off; - - if (visSelLo < 0) { visSelLo = 0; } - if (visSelHi > dispLen) { visSelHi = dispLen; } - - if (selLo >= 0 && visSelLo < visSelHi) { - // Before selection - if (visSelLo > 0) { - drawTextN(d, ops, font, textX, textY, dispBuf, visSelLo, fg, bg, true); - } - - // Selection - drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, dispBuf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); - - // After selection - if (visSelHi < dispLen) { - drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, dispBuf + visSelHi, dispLen - visSelHi, fg, bg, true); - } - } else { - // No selection — single run - drawTextN(d, ops, font, textX, textY, dispBuf, dispLen, fg, bg, true); - } - - // Draw cursor (blinks at same rate as terminal cursor) - if (w->focused && w->enabled && sCursorBlinkOn) { - int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth; - - if (cursorX >= w->x + TEXT_INPUT_PAD && - cursorX < w->x + w->w - TEXT_INPUT_PAD) { - drawVLine(d, ops, cursorX, textY, font->charHeight, fg); - } - } + widgetTextEditPaintLine(d, ops, font, colors, textX, textY, dispBuf, dispLen, off, w->as.textInput.cursorPos, w->as.textInput.selStart, w->as.textInput.selEnd, fg, bg, w->focused && w->enabled, w->x + TEXT_INPUT_PAD, w->x + w->w - TEXT_INPUT_PAD); } }