More text editing code consolidated.

This commit is contained in:
Scott Duensing 2026-03-18 00:17:25 -05:00
parent 73a54a8b3a
commit 83860ac13d
4 changed files with 165 additions and 288 deletions

View file

@ -291,46 +291,7 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
clearOtherSelections(w); clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData; AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font; 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);
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;
} }
} }
@ -370,49 +331,7 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit
len = maxChars; len = maxChars;
} }
// Selection range 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);
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);
}
}
} }
// Drop button // Drop button

View file

@ -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 // (which store these fields in different union members). This avoids
// duplicating the full editing logic (cursor movement, word selection, // duplicating the full editing logic (cursor movement, word selection,
// clipboard, undo) three times. // 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 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 widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
int32_t wordEnd(const char *buf, int32_t len, int32_t pos); int32_t wordEnd(const char *buf, int32_t len, int32_t pos);

View file

@ -305,34 +305,8 @@ void widgetSpinnerOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
} }
} else { } else {
// Click on text area // Click on text area
AppContextT *ctx = (AppContextT *)root->userData; AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font; 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);
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;
}
spinnerStartEdit(hit); spinnerStartEdit(hit);
} }
@ -398,52 +372,7 @@ void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitm
len = 0; len = 0;
} }
// Selection range 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);
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);
}
}
// Up button (top half) // Up button (top half)
int32_t btnTopH = w->h / 2; int32_t btnTopH = w->h / 2;

View file

@ -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 // Multi-click tracking
// ============================================================ // ============================================================
@ -1830,30 +1979,9 @@ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
if (w->type == WidgetTextInputE) { if (w->type == WidgetTextInputE) {
int32_t leftEdge = w->x + TEXT_INPUT_PAD; int32_t leftEdge = w->x + TEXT_INPUT_PAD;
int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth;
int32_t rightEdge = leftEdge + maxChars * font->charWidth; widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.textInput.len, &w->as.textInput.cursorPos, &w->as.textInput.scrollOff, &w->as.textInput.selEnd);
// 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;
} else if (w->type == WidgetTextAreaE) { } else if (w->type == WidgetTextAreaE) {
int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD;
int32_t innerY = w->y + TEXTAREA_BORDER; 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.desiredCol = clickCol;
w->as.textArea.selCursor = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); w->as.textArea.selCursor = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol);
} else if (w->type == WidgetComboBoxE) { } else if (w->type == WidgetComboBoxE) {
int32_t leftEdge = w->x + TEXT_INPUT_PAD; int32_t leftEdge = w->x + TEXT_INPUT_PAD;
int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth; int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth;
int32_t rightEdge = leftEdge + maxChars * font->charWidth; widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.comboBox.len, &w->as.comboBox.cursorPos, &w->as.comboBox.scrollOff, &w->as.comboBox.selEnd);
// 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;
} else if (w->type == WidgetAnsiTermE) { } else if (w->type == WidgetAnsiTermE) {
int32_t baseX = w->x + 2; // ANSI_BORDER int32_t baseX = w->x + 2; // ANSI_BORDER
int32_t baseY = w->y + 2; int32_t baseY = w->y + 2;
@ -2360,46 +2467,7 @@ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
clearOtherSelections(w); clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData; AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font; 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);
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;
} }
@ -2440,15 +2508,6 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
len = maxChars; 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); bool isPassword = (w->as.textInput.inputMode == InputPasswordE);
// Build display buffer (password masking) // 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); memcpy(dispBuf, w->as.textInput.buf + off, dispLen);
} }
// Draw up to 3 runs: before selection, selection, after selection 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);
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);
}
}
} }
} }