768 lines
21 KiB
C
768 lines
21 KiB
C
// textHelp.c -- Shared text editing infrastructure
|
|
//
|
|
// Provides clipboard, multi-click detection, word boundary logic,
|
|
// cross-widget selection clearing, and the single-line text editing
|
|
// engine. Shared across multiple widget DXEs (TextInput, TextArea,
|
|
// ComboBox, Spinner, AnsiTerm) via the texthelp.lib DXE.
|
|
|
|
#include "textHelp.h"
|
|
|
|
#include <ctype.h>
|
|
#include <time.h>
|
|
|
|
// ============================================================
|
|
// Static state
|
|
// ============================================================
|
|
|
|
// Cursor blink state
|
|
#define CURSOR_BLINK_MS 250
|
|
static clock_t sCursorBlinkTime = 0;
|
|
|
|
// Track the widget that last had an active selection so we can
|
|
// clear it in O(1) instead of walking every widget in every window.
|
|
static WidgetT *sLastSelectedWidget = NULL;
|
|
|
|
// ============================================================
|
|
// Static helper prototypes
|
|
// ============================================================
|
|
|
|
static bool clearSelectionOnWidget(WidgetT *w);
|
|
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
|
|
static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
|
|
static int32_t wordBoundaryLeft(const char *buf, int32_t pos);
|
|
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
|
|
|
|
|
|
// ============================================================
|
|
// wgtUpdateCursorBlink
|
|
// ============================================================
|
|
|
|
static void textHelpInit(void) __attribute__((constructor));
|
|
static void textHelpInit(void) {
|
|
sCursorBlinkFn = wgtUpdateCursorBlink;
|
|
}
|
|
|
|
|
|
void wgtUpdateCursorBlink(void) {
|
|
clock_t now = clock();
|
|
clock_t interval = (clock_t)CURSOR_BLINK_MS * CLOCKS_PER_SEC / 1000;
|
|
|
|
if ((now - sCursorBlinkTime) >= interval) {
|
|
sCursorBlinkTime = now;
|
|
sCursorBlinkOn = !sCursorBlinkOn;
|
|
|
|
if (sFocusedWidget) {
|
|
wgtInvalidatePaint(sFocusedWidget);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// clearOtherSelections
|
|
// ============================================================
|
|
//
|
|
// Clears selection on the previously-selected widget (if different
|
|
// from the newly-focused one). Validates that the previous widget's
|
|
// window is still in the window stack before touching it -- the
|
|
// window may have been closed since sLastSelectedWidget was set.
|
|
// If the previous widget was in a different window, that window
|
|
// gets a full repaint to clear the stale selection highlight.
|
|
|
|
void clearOtherSelections(WidgetT *except) {
|
|
if (!except || !except->window || !except->window->widgetRoot) {
|
|
return;
|
|
}
|
|
|
|
WidgetT *prev = sLastSelectedWidget;
|
|
sLastSelectedWidget = except;
|
|
|
|
if (!prev || prev == except) {
|
|
return;
|
|
}
|
|
|
|
// Verify the widget is still alive (its window still in the stack)
|
|
WindowT *prevWin = prev->window;
|
|
|
|
if (!prevWin) {
|
|
return;
|
|
}
|
|
|
|
AppContextT *ctx = wgtGetContext(except);
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
bool found = false;
|
|
|
|
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
|
if (ctx->stack.windows[i] == prevWin) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
return;
|
|
}
|
|
|
|
if (clearSelectionOnWidget(prev) && prevWin != except->window) {
|
|
dvxInvalidateWindow(ctx, prevWin);
|
|
}
|
|
}
|
|
|
|
|
|
static bool clearSelectionOnWidget(WidgetT *w) {
|
|
if (w->wclass && w->wclass->clearSelection) {
|
|
return w->wclass->clearSelection(w);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
|
|
bool isWordChar(char c) {
|
|
return isalnum((unsigned char)c) || c == '_';
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Shared undo/selection helpers
|
|
// ============================================================
|
|
|
|
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) {
|
|
int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd;
|
|
int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart;
|
|
|
|
if (lo < 0) {
|
|
lo = 0;
|
|
}
|
|
|
|
if (hi > *pLen) {
|
|
hi = *pLen;
|
|
}
|
|
|
|
if (lo >= hi) {
|
|
*pSelStart = -1;
|
|
*pSelEnd = -1;
|
|
return;
|
|
}
|
|
|
|
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
|
*pLen -= (hi - lo);
|
|
*pCursor = lo;
|
|
*pSelStart = -1;
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
|
|
static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize) {
|
|
if (!undoBuf) {
|
|
return;
|
|
}
|
|
|
|
int32_t copyLen = len < bufSize ? len : bufSize - 1;
|
|
memcpy(undoBuf, buf, copyLen);
|
|
undoBuf[copyLen] = '\0';
|
|
*pUndoLen = copyLen;
|
|
*pUndoCursor = cursor;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTextEditDragUpdateLine
|
|
// ============================================================
|
|
//
|
|
// Called during mouse drag to extend the selection for single-line
|
|
// text widgets. Auto-scrolls when the mouse moves past the visible
|
|
// text edges.
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTextEditMouseClick
|
|
// ============================================================
|
|
//
|
|
// Computes cursor position from pixel coordinates, handles multi-click
|
|
// (double = word select, triple = select all), and optionally starts
|
|
// drag-select.
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTextEditOnKey -- shared single-line text editing logic
|
|
// ============================================================
|
|
//
|
|
// This is the core single-line text editing engine, parameterized by
|
|
// pointer to allow reuse across TextInput, Spinner, and ComboBox.
|
|
|
|
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) {
|
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
|
bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd);
|
|
int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1;
|
|
int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1;
|
|
|
|
// Clamp selection to buffer bounds
|
|
if (hasSel) {
|
|
if (selLo < 0) {
|
|
selLo = 0;
|
|
}
|
|
|
|
if (selHi > *pLen) {
|
|
selHi = *pLen;
|
|
}
|
|
|
|
if (selLo >= selHi) {
|
|
hasSel = false;
|
|
selLo = -1;
|
|
selHi = -1;
|
|
}
|
|
}
|
|
|
|
// Ctrl+A -- select all
|
|
if (key == 1 && pSelStart && pSelEnd) {
|
|
*pSelStart = 0;
|
|
*pSelEnd = *pLen;
|
|
*pCursor = *pLen;
|
|
goto adjustScroll;
|
|
}
|
|
|
|
// Ctrl+C -- copy
|
|
if (key == 3) {
|
|
if (hasSel) {
|
|
clipboardCopy(buf + selLo, selHi - selLo);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Ctrl+V -- paste
|
|
if (key == 22) {
|
|
int32_t cbLen = 0;
|
|
const char *cb = clipboardGet(&cbLen);
|
|
|
|
if (cbLen > 0) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
if (hasSel) {
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
}
|
|
|
|
int32_t canFit = bufSize - 1 - *pLen;
|
|
// For single-line, skip newlines in clipboard
|
|
int32_t paste = 0;
|
|
|
|
for (int32_t i = 0; i < cbLen && paste < canFit; i++) {
|
|
if (cb[i] != '\n' && cb[i] != '\r') {
|
|
paste++;
|
|
}
|
|
}
|
|
|
|
if (paste > 0) {
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos + paste, buf + pos, *pLen - pos + 1);
|
|
|
|
int32_t j = 0;
|
|
|
|
for (int32_t i = 0; i < cbLen && j < paste; i++) {
|
|
if (cb[i] != '\n' && cb[i] != '\r') {
|
|
buf[pos + j] = cb[i];
|
|
j++;
|
|
}
|
|
}
|
|
|
|
*pLen += paste;
|
|
*pCursor += paste;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
goto adjustScroll;
|
|
}
|
|
|
|
// Ctrl+X -- cut
|
|
if (key == 24) {
|
|
if (hasSel) {
|
|
clipboardCopy(buf + selLo, selHi - selLo);
|
|
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
goto adjustScroll;
|
|
}
|
|
|
|
// Ctrl+Z -- undo
|
|
if (key == 26 && undoBuf && pUndoLen && pUndoCursor) {
|
|
// Swap current and undo
|
|
char tmpBuf[clipboardMaxLen() + 1];
|
|
int32_t tmpLen = *pLen;
|
|
int32_t tmpCursor = *pCursor;
|
|
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;
|
|
|
|
memcpy(tmpBuf, buf, copyLen);
|
|
tmpBuf[copyLen] = '\0';
|
|
|
|
int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1;
|
|
memcpy(buf, undoBuf, restLen);
|
|
buf[restLen] = '\0';
|
|
*pLen = restLen;
|
|
|
|
int32_t restoreCursor = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;
|
|
|
|
// Save old as new undo
|
|
int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
|
|
memcpy(undoBuf, tmpBuf, saveLen);
|
|
undoBuf[saveLen] = '\0';
|
|
*pUndoLen = saveLen;
|
|
*pUndoCursor = tmpCursor;
|
|
|
|
*pCursor = restoreCursor;
|
|
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
goto adjustScroll;
|
|
}
|
|
|
|
if (key >= 32 && key < 127) {
|
|
// Printable character
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
if (hasSel) {
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
}
|
|
|
|
if (*pLen < bufSize - 1) {
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos + 1, buf + pos, *pLen - pos + 1);
|
|
buf[pos] = (char)key;
|
|
(*pLen)++;
|
|
(*pCursor)++;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else if (key == 8) {
|
|
// Backspace
|
|
if (hasSel) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
} else if (*pCursor > 0) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos - 1, buf + pos, *pLen - pos + 1);
|
|
(*pLen)--;
|
|
(*pCursor)--;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else if (key == (0x4B | 0x100)) {
|
|
// Left arrow
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
*pSelEnd = *pCursor;
|
|
}
|
|
|
|
if (*pCursor > 0) {
|
|
(*pCursor)--;
|
|
}
|
|
|
|
*pSelEnd = *pCursor;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
if (*pCursor > 0) {
|
|
(*pCursor)--;
|
|
}
|
|
}
|
|
} else if (key == (0x4D | 0x100)) {
|
|
// Right arrow
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
*pSelEnd = *pCursor;
|
|
}
|
|
|
|
if (*pCursor < *pLen) {
|
|
(*pCursor)++;
|
|
}
|
|
|
|
*pSelEnd = *pCursor;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
if (*pCursor < *pLen) {
|
|
(*pCursor)++;
|
|
}
|
|
}
|
|
} else if (key == (0x73 | 0x100)) {
|
|
// Ctrl+Left -- word left
|
|
int32_t newPos = wordBoundaryLeft(buf, *pCursor);
|
|
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
*pSelEnd = *pCursor;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
*pSelEnd = newPos;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
}
|
|
} else if (key == (0x74 | 0x100)) {
|
|
// Ctrl+Right -- word right
|
|
int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor);
|
|
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
*pSelEnd = *pCursor;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
*pSelEnd = newPos;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
}
|
|
} else if (key == (0x47 | 0x100)) {
|
|
// Home
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
*pSelEnd = *pCursor;
|
|
}
|
|
|
|
*pCursor = 0;
|
|
*pSelEnd = 0;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = 0;
|
|
}
|
|
} else if (key == (0x4F | 0x100)) {
|
|
// End
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
*pSelEnd = *pCursor;
|
|
}
|
|
|
|
*pCursor = *pLen;
|
|
*pSelEnd = *pLen;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = *pLen;
|
|
}
|
|
} else if (key == (0x53 | 0x100)) {
|
|
// Delete
|
|
if (hasSel) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
} else if (*pCursor < *pLen) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos, buf + pos + 1, *pLen - pos);
|
|
(*pLen)--;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
adjustScroll:
|
|
// Adjust scroll offset to keep cursor visible
|
|
{
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t fieldW = w->w;
|
|
|
|
if (w->wclass && w->wclass->getTextFieldWidth) {
|
|
fieldW = w->wclass->getTextFieldWidth(w);
|
|
}
|
|
|
|
int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth;
|
|
|
|
if (*pCursor < *pScrollOff) {
|
|
*pScrollOff = *pCursor;
|
|
}
|
|
|
|
if (*pCursor >= *pScrollOff + visibleChars) {
|
|
*pScrollOff = *pCursor - visibleChars + 1;
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTextEditPaintLine
|
|
// ============================================================
|
|
//
|
|
// Renders a single line of text with optional selection highlighting
|
|
// and a blinking cursor.
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Word boundary helpers
|
|
// ============================================================
|
|
|
|
static int32_t wordBoundaryLeft(const char *buf, int32_t pos) {
|
|
if (pos <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
// Skip non-word characters
|
|
while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') {
|
|
pos--;
|
|
}
|
|
|
|
// Skip word characters
|
|
while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) {
|
|
pos--;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) {
|
|
if (pos >= len) {
|
|
return len;
|
|
}
|
|
|
|
// Skip word characters
|
|
while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) {
|
|
pos++;
|
|
}
|
|
|
|
// Skip non-word characters
|
|
while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') {
|
|
pos++;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
int32_t wordEnd(const char *buf, int32_t len, int32_t pos) {
|
|
while (pos < len && isWordChar(buf[pos])) {
|
|
pos++;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
int32_t wordStart(const char *buf, int32_t pos) {
|
|
while (pos > 0 && isWordChar(buf[pos - 1])) {
|
|
pos--;
|
|
}
|
|
|
|
return pos;
|
|
}
|