// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // 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 #include // ============================================================ // Static state // ============================================================ // Cursor blink state #define CURSOR_BLINK_MS 250 static clock_t sCursorBlinkTime = 0; // Slack added to line cache capacity when growing. Bigger values // mean fewer reallocs at the cost of wasted space; 256 empirically // keeps realloc traffic low on medium-sized file edits. #define TEXT_EDIT_LINE_CAP_GROWTH 256 // Default tab width when a cache has been left at 0 (callers should // always initialize with textEditLineCacheInit, but guard defensively). #define TEXT_EDIT_DEFAULT_TAB_WIDTH 3 // 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; // ============================================================ // Prototypes // ============================================================ void clearOtherSelections(WidgetT *except); static bool clearSelectionOnWidget(WidgetT *w); bool isWordChar(char c); static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors); void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol); bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor); int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize); void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor); void textEditLineCacheDirty(TextEditLineCacheT *lc); void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len); void textEditLineCacheFree(TextEditLineCacheT *lc); void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth); void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen); void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen); static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len); void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth); void textEditMultiFree(TextEditMultiT *te); bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth); int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len); int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row); int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len); void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col); int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col); void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom); int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW); int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW); static void textHelpInit(void) __attribute__((constructor)); void wgtUpdateCursorBlink(void); 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 widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor); void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor); void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts); void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks); void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll); int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff); int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff); 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, int32_t fieldWidth); 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); int32_t wordBoundaryLeft(const char *buf, int32_t pos); int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); int32_t wordEnd(const char *buf, int32_t len, int32_t pos); int32_t wordStart(const char *buf, int32_t pos); // 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) { return wclsClearSelection(w); } bool isWordChar(char c) { return isalnum((unsigned char)c) || c == '_'; } 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; } // Paint `text` as a run of single-color regions defined by // syntaxColors[textOff..textOff+len). Coalesces adjacent same-color // bytes to minimize drawTextN calls. void textEditDrawColorizedText(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t len, const uint8_t *syntaxColors, int32_t textOff, uint32_t defaultFg, uint32_t bg, const uint32_t *customColors) { int32_t runStart = 0; while (runStart < len) { uint8_t curColor = syntaxColors[textOff + runStart]; int32_t runEnd = runStart + 1; while (runEnd < len && syntaxColors[textOff + runEnd] == curColor) { runEnd++; } uint32_t fg = textEditSyntaxColor(d, curColor, defaultFg, customColors); drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true); runStart = runEnd; } } void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol) { int32_t curLineOff = textEditLineStart(lc, buf, len, cursorRow); int32_t curOff = curLineOff + cursorCol; if (curOff > len) { curOff = len; } int32_t col = textEditVisualCol(buf, curLineOff, curOff, tabW); if (cursorRow < *pScrollRow) { *pScrollRow = cursorRow; } if (cursorRow >= *pScrollRow + visRows) { *pScrollRow = cursorRow - visRows + 1; } if (col < *pScrollCol) { *pScrollCol = col; } if (col >= *pScrollCol + visCols) { *pScrollCol = col - visCols + 1; } } // Searches forward (or backward) from the cursor for `needle`. On // match, selects it and moves the cursor to match start (forward) // or match end (backward). No wrap-around. bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor) { if (!needle || !needle[0]) { return false; } int32_t needleLen = strlen(needle); if (needleLen > len) { return false; } int32_t cursorByte = textEditRowColToOff(lc, buf, len, *pCursorRow, *pCursorCol); int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1; int32_t searchLen = len - needleLen + 1; // >= 1 given the needleLen > len check above int32_t count = forward ? (searchLen - startPos) : (startPos + 1); if (count <= 0) { return false; } for (int32_t attempt = 0; attempt < count; attempt++) { int32_t pos = forward ? startPos + attempt : startPos - attempt; if (pos < 0 || pos >= searchLen) { break; } bool match; if (caseSensitive) { match = (memcmp(buf + pos, needle, needleLen) == 0); } else { match = (strncasecmp(buf + pos, needle, needleLen) == 0); } if (match) { *pSelAnchor = pos; *pSelCursor = pos + needleLen; int32_t cursorOff = forward ? pos : pos + needleLen; textEditOffToRowCol(lc, buf, len, cursorOff, pCursorRow, pCursorCol); *pDesiredCol = *pCursorCol; return true; } } return false; } // Writes the word under the cursor (if any) into out and returns // its length. Empty string if the cursor is not on a word. int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize) { out[0] = '\0'; if (!buf || len == 0 || outSize < 2) { return 0; } int32_t off = textEditRowColToOff(lc, buf, len, cursorRow, cursorCol); int32_t ws = wordStart(buf, off); int32_t we = wordEnd(buf, len, off); int32_t wdLen = we - ws; if (wdLen <= 0) { return 0; } if (wdLen >= outSize) { wdLen = outSize - 1; } memcpy(out, buf + ws, wdLen); out[wdLen] = '\0'; return wdLen; } // Jumps to the given 1-based line, selects the whole line, and // scrolls so the line sits ~1/4 from the top of the visible area. void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor) { int32_t totalLines = textEditLineCount(lc, buf, len); int32_t row = line - 1; if (row < 0) { row = 0; } if (row >= totalLines) { row = totalLines - 1; } *pCursorRow = row; *pCursorCol = 0; *pDesiredCol = 0; int32_t lineStart = textEditLineStart(lc, buf, len, row); int32_t lineL = textEditLineLen(lc, buf, len, row); *pSelAnchor = lineStart; *pSelCursor = lineStart + lineL; int32_t targetScroll = row - visRows / 4; if (targetScroll < 0) { targetScroll = 0; } int32_t maxScroll = totalLines - visRows; if (maxScroll < 0) { maxScroll = 0; } if (targetScroll > maxScroll) { targetScroll = maxScroll; } *pScrollRow = targetScroll; *pScrollCol = 0; } void textEditLineCacheDirty(TextEditLineCacheT *lc) { lc->cachedLines = -1; lc->cachedMaxLL = -1; } void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len) { if (lc->cachedLines < 0) { textEditLineCacheRebuild(lc, buf, len); } } void textEditLineCacheFree(TextEditLineCacheT *lc) { free(lc->lineOffsets); free(lc->lineVisLens); lc->lineOffsets = NULL; lc->lineVisLens = NULL; lc->lineOffsetCap = 0; lc->lineVisLenCap = 0; lc->cachedLines = -1; lc->cachedMaxLL = -1; } void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth) { lc->lineOffsets = NULL; lc->lineVisLens = NULL; lc->lineOffsetCap = 0; lc->lineVisLenCap = 0; lc->cachedLines = -1; lc->cachedMaxLL = -1; lc->tabWidth = tabWidth > 0 ? tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; } // Incrementally update the cache after deleting bytes at `off`. // Caller passes the POST-delete buffer and length. Falls back to a // full rebuild if the deletion may have spanned a newline. void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen) { if (lc->cachedLines < 0 || deleteLen <= 0) { textEditLineCacheDirty(lc); return; } // We only trust the incremental path for single-byte deletes; any // longer run could have swallowed a newline, and the post-delete // buffer can't tell us whether it did. if (deleteLen > 1) { textEditLineCacheDirty(lc); return; } int32_t line = 0; for (line = lc->cachedLines - 1; line > 0; line--) { if (lc->lineOffsets[line] <= off) { break; } } for (int32_t i = line + 1; i <= lc->cachedLines; i++) { lc->lineOffsets[i] -= deleteLen; } int32_t lineOff = lc->lineOffsets[line]; int32_t nextOff = (line + 1 <= lc->cachedLines) ? lc->lineOffsets[line + 1] : len; if (line + 1 <= lc->cachedLines && nextOff <= lineOff) { textEditLineCacheDirty(lc); return; } for (int32_t i = lineOff; i < nextOff && i < len; i++) { if (buf[i] == '\n') { if (i < nextOff - 1) { textEditLineCacheDirty(lc); return; } } } int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; int32_t vc = 0; for (int32_t i = lineOff; i < nextOff && i < len && buf[i] != '\n'; i++) { if (buf[i] == '\t') { vc += tabW - (vc % tabW); } else { vc++; } } lc->lineVisLens[line] = vc; lc->cachedMaxLL = -1; } // Incrementally update the cache after inserting bytes at `off`. // Caller passes the POST-insert buffer and length. Falls back to a // full rebuild if the insertion contains newlines. void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen) { if (lc->cachedLines < 0 || insertLen <= 0) { textEditLineCacheDirty(lc); return; } for (int32_t i = off; i < off + insertLen && i < len; i++) { if (buf[i] == '\n') { textEditLineCacheDirty(lc); return; } } int32_t line = 0; for (line = lc->cachedLines - 1; line > 0; line--) { if (lc->lineOffsets[line] <= off) { break; } } for (int32_t i = line + 1; i <= lc->cachedLines; i++) { lc->lineOffsets[i] += insertLen; } int32_t lineOff = lc->lineOffsets[line]; int32_t lineEnd = (line + 1 <= lc->cachedLines) ? lc->lineOffsets[line + 1] : len; int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; int32_t vc = 0; for (int32_t i = lineOff; i < lineEnd && buf[i] != '\n'; i++) { if (buf[i] == '\t') { vc += tabW - (vc % tabW); } else { vc++; } } lc->lineVisLens[line] = vc; if (vc > lc->cachedMaxLL) { lc->cachedMaxLL = vc; } else { lc->cachedMaxLL = -1; } } // Single O(N) scan populates line-offset table plus per-line // visual lengths. lineOffsets[lineCount] is the buffer-end sentinel. static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len) { int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; int32_t lineCount = 1; for (int32_t i = 0; i < len; i++) { if (buf[i] == '\n') { lineCount++; } } int32_t needed = lineCount + 1; if (needed > lc->lineOffsetCap) { int32_t newCap = needed + TEXT_EDIT_LINE_CAP_GROWTH; lc->lineOffsets = (int32_t *)realloc(lc->lineOffsets, newCap * sizeof(int32_t)); lc->lineOffsetCap = newCap; } if (lineCount > lc->lineVisLenCap) { int32_t newCap = lineCount + TEXT_EDIT_LINE_CAP_GROWTH; lc->lineVisLens = (int32_t *)realloc(lc->lineVisLens, newCap * sizeof(int32_t)); lc->lineVisLenCap = newCap; } int32_t line = 0; int32_t vc = 0; int32_t maxVL = 0; lc->lineOffsets[0] = 0; for (int32_t i = 0; i < len; i++) { if (buf[i] == '\n') { lc->lineVisLens[line] = vc; if (vc > maxVL) { maxVL = vc; } line++; lc->lineOffsets[line] = i + 1; vc = 0; } else if (buf[i] == '\t') { vc += tabW - (vc % tabW); } else { vc++; } } lc->lineVisLens[line] = vc; if (vc > maxVL) { maxVL = vc; } lc->lineOffsets[lineCount] = len; lc->cachedLines = lineCount; lc->cachedMaxLL = maxVL; } void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth) { int32_t newW = tabWidth > 0 ? tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; if (lc->tabWidth == newW) { return; } lc->tabWidth = newW; textEditLineCacheDirty(lc); } void textEditMultiFree(TextEditMultiT *te) { free(te->buf); free(te->undoBuf); textEditLineCacheFree(&te->lines); te->buf = NULL; te->undoBuf = NULL; te->bufSize = 0; te->len = 0; } // Allocates the buf/undoBuf pair sized for maxLen characters plus // NUL, and initializes cursor/scroll/selection/undo state. Returns // false (leaving te->buf == NULL) if allocation fails. bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth) { memset(te, 0, sizeof(*te)); int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; te->buf = (char *)malloc(bufSize); te->undoBuf = (char *)malloc(bufSize); te->bufSize = bufSize; if (!te->buf || !te->undoBuf) { free(te->buf); free(te->undoBuf); te->buf = NULL; te->undoBuf = NULL; return false; } te->buf[0] = '\0'; te->undoBuf[0] = '\0'; te->selAnchor = -1; te->selCursor = -1; textEditLineCacheInit(&te->lines, tabWidth); return true; } int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len) { textEditLineCacheEnsure(lc, buf, len); return lc->cachedLines; } int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row) { textEditLineCacheEnsure(lc, buf, len); if (row < 0 || row >= lc->cachedLines) { return 0; } int32_t start = lc->lineOffsets[row]; int32_t next = lc->lineOffsets[row + 1]; if (next > start && buf[next - 1] == '\n') { next--; } return next - start; } int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row) { textEditLineCacheEnsure(lc, buf, len); if (row < 0) { return 0; } if (row >= lc->cachedLines) { return len; } return lc->lineOffsets[row]; } int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len) { textEditLineCacheEnsure(lc, buf, len); if (lc->cachedMaxLL < 0 && lc->cachedLines >= 0) { int32_t maxVL = 0; for (int32_t i = 0; i < lc->cachedLines; i++) { if (lc->lineVisLens[i] > maxVL) { maxVL = lc->lineVisLens[i]; } } lc->cachedMaxLL = maxVL; } return lc->cachedMaxLL; } void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col) { textEditLineCacheEnsure(lc, buf, len); if (!lc->lineOffsets || lc->cachedLines < 0) { int32_t r = 0; int32_t c = 0; for (int32_t i = 0; i < off; i++) { if (buf[i] == '\n') { r++; c = 0; } else { c++; } } *row = r; *col = c; return; } int32_t lo = 0; int32_t hi = lc->cachedLines; while (lo < hi) { int32_t mid = (lo + hi + 1) / 2; if (lc->lineOffsets[mid] <= off) { lo = mid; } else { hi = mid - 1; } } *row = lo; *col = off - lc->lineOffsets[lo]; } // Replaces every occurrence of needle with replacement. Records a // single undo snapshot before mutating. Clamps the cursor to the // post-replace buffer length and clears any selection. Returns the // count of replacements made. int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) { if (!needle || !needle[0] || !replacement) { return 0; } int32_t needleLen = strlen(needle); int32_t replLen = strlen(replacement); int32_t delta = replLen - needleLen; int32_t count = 0; if (needleLen > *pLen) { return 0; } textEditSaveUndo(buf, *pLen, textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol), undoBuf, pUndoLen, pUndoCursor, bufSize); int32_t pos = 0; while (pos + needleLen <= *pLen) { bool match; if (caseSensitive) { match = (memcmp(buf + pos, needle, needleLen) == 0); } else { match = (strncasecmp(buf + pos, needle, needleLen) == 0); } if (match) { if (*pLen + delta >= bufSize) { break; } if (delta != 0) { memmove(buf + pos + replLen, buf + pos + needleLen, *pLen - pos - needleLen); } memcpy(buf + pos, replacement, replLen); *pLen += delta; buf[*pLen] = '\0'; count++; pos += replLen; } else { pos++; } } if (count > 0) { *pSelAnchor = -1; *pSelCursor = -1; int32_t cursorOff = textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol); if (cursorOff > *pLen) { textEditOffToRowCol(lc, buf, *pLen, *pLen, pCursorRow, pCursorCol); } *pDesiredCol = *pCursorCol; textEditLineCacheDirty(lc); } return count; } int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col) { int32_t start = textEditLineStart(lc, buf, len, row); int32_t lineL = textEditLineLen(lc, buf, len, row); int32_t clamp = col < lineL ? col : lineL; return start + clamp; } 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; } // Resolve a syntax color index to a packed-pixel foreground color. // Falls back to defaultFg for TEXT_SYNTAX_DEFAULT or unknown indices. // A non-zero entry in custom[idx] (0x00RRGGBB) overrides the default. uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom) { if (idx != TEXT_SYNTAX_DEFAULT && idx < TEXT_SYNTAX_MAX && custom && custom[idx]) { uint32_t c = custom[idx]; return packColor(d, (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF); } switch (idx) { case TEXT_SYNTAX_KEYWORD: return packColor(d, 0, 0, 128); case TEXT_SYNTAX_STRING: return packColor(d, 128, 0, 0); case TEXT_SYNTAX_COMMENT: return packColor(d, 0, 128, 0); case TEXT_SYNTAX_NUMBER: return packColor(d, 128, 0, 128); case TEXT_SYNTAX_OPERATOR: return packColor(d, 128, 128, 0); case TEXT_SYNTAX_TYPE: return packColor(d, 0, 128, 128); default: return defaultFg; } } // Tab-aware visual column from buffer offset within a line. // tabW <= 0 means tabs are 1 column (no expansion). int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW) { int32_t vc = 0; for (int32_t i = lineStart; i < off; i++) { if (buf[i] == '\t' && tabW > 0) { vc += tabW - (vc % tabW); } else { vc++; } } return vc; } // Tab-aware: convert visual column to buffer offset within a line. int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW) { int32_t vc = 0; int32_t i = lineStart; while (i < len && buf[i] != '\n') { int32_t w = 1; if (buf[i] == '\t' && tabW > 0) { w = tabW - (vc % tabW); } if (vc + w > targetVC) { break; } vc += w; i++; } return i; } 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); } } } // 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; } // 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; sDragWidget = 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; } sDragWidget = NULL; return; } // Single click: place cursor *pCursorPos = charPos; *pSelStart = charPos; *pSelEnd = charPos; sDragWidget = dragSelect ? w : NULL; } // Called during a drag to auto-scroll when the mouse moves past the // top or bottom of the visible area, and to extend the selection to // the new cursor position. void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor) { int32_t relX = vx - textX; int32_t relY = vy - textY; int32_t totalLines = textEditLineCount(lc, buf, len); if (relY < 0 && *pScrollRow > 0) { (*pScrollRow)--; } else if (relY >= visRows * font->charHeight && *pScrollRow < totalLines - visRows) { (*pScrollRow)++; } int32_t clickRow = *pScrollRow + relY / font->charHeight; int32_t clickVisCol = scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= totalLines) { clickRow = totalLines - 1; } if (clickVisCol < 0) { clickVisCol = 0; } int32_t dragLineStart = textEditLineStart(lc, buf, len, clickRow); int32_t dragByteOff = textEditVisualColToOff(buf, len, dragLineStart, clickVisCol, tabW); int32_t clickCol = dragByteOff - dragLineStart; int32_t lineL = textEditLineLen(lc, buf, len, clickRow); if (clickCol > lineL) { clickCol = lineL; } *pCursorRow = clickRow; *pCursorCol = clickCol; *pDesiredCol = clickCol; *pSelCursor = textEditRowColToOff(lc, buf, len, clickRow, clickCol); } // Handle a mouse click on the text content area. Caller has already // excluded scrollbar and gutter clicks. Single click places cursor // and starts a drag anchor; double-click selects the word under // the cursor; triple-click selects the whole line. void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor) { int32_t totalLines = textEditLineCount(lc, buf, len); int32_t relX = vx - textX; int32_t relY = vy - textY; int32_t clickRow = scrollRow + relY / font->charHeight; int32_t clickVisCol = scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= totalLines) { clickRow = totalLines - 1; } if (clickVisCol < 0) { clickVisCol = 0; } int32_t clkLineStart = textEditLineStart(lc, buf, len, clickRow); int32_t clkByteOff = textEditVisualColToOff(buf, len, clkLineStart, clickVisCol, tabW); int32_t clickCol = clkByteOff - clkLineStart; int32_t lineL = textEditLineLen(lc, buf, len, clickRow); if (clickCol > lineL) { clickCol = lineL; } int32_t clicks = multiClickDetect(vx, vy); if (clicks >= 3) { int32_t lineStart = textEditRowColToOff(lc, buf, len, clickRow, 0); int32_t lineEnd = textEditRowColToOff(lc, buf, len, clickRow, lineL); *pCursorRow = clickRow; *pCursorCol = lineL; *pDesiredCol = lineL; *pSelAnchor = lineStart; *pSelCursor = lineEnd; sDragWidget = NULL; return; } if (clicks == 2 && buf) { int32_t off = textEditRowColToOff(lc, buf, len, clickRow, clickCol); int32_t ws = wordStart(buf, off); int32_t we = wordEnd(buf, len, off); int32_t weRow; int32_t weCol; textEditOffToRowCol(lc, buf, len, we, &weRow, &weCol); *pCursorRow = weRow; *pCursorCol = weCol; *pDesiredCol = weCol; *pSelAnchor = ws; *pSelCursor = we; sDragWidget = NULL; return; } *pCursorRow = clickRow; *pCursorCol = clickCol; *pDesiredCol = clickCol; int32_t anchorOff = textEditRowColToOff(lc, buf, len, clickRow, clickCol); *pSelAnchor = anchorOff; *pSelCursor = anchorOff; sDragWidget = w; } // Core multi-line text editing engine. Parameterized on state // pointers so that the widget's fields remain its own; the library // never dereferences TextAreaDataT or similar. void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts) { int32_t *pRow = pCursorRow; int32_t *pCol = pCursorCol; int32_t *pSA = pSelAnchor; int32_t *pSC = pSelCursor; bool shift = (mod & KEY_MOD_SHIFT) != 0; int32_t tabW = opts->tabWidth > 0 ? opts->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH; #define CUR_OFF() textEditRowColToOff(lc, buf, *pLen, *pRow, *pCol) #define ENSURE_VIS() textEditEnsureVisible(lc, buf, *pLen, *pRow, *pCol, tabW, visRows, visCols, pScrollRow, pScrollCol) #define FROM_OFF(o) textEditOffToRowCol(lc, buf, *pLen, (o), pRow, pCol) #define SEL_BEGIN() do { \ if (shift && *pSA < 0) { *pSA = CUR_OFF(); *pSC = *pSA; } \ } while (0) #define SEL_END() do { \ if (shift) { *pSC = CUR_OFF(); } \ else { *pSA = -1; *pSC = -1; } \ } while (0) #define HAS_SEL() (*pSA >= 0 && *pSC >= 0 && *pSA != *pSC) #define SEL_LO() (*pSA < *pSC ? *pSA : *pSC) #define SEL_HI() (*pSA < *pSC ? *pSC : *pSA) if (HAS_SEL()) { if (*pSA > *pLen) { *pSA = *pLen; } if (*pSC > *pLen) { *pSC = *pLen; } } // Ctrl+A -- select all if (key == 1) { *pSA = 0; *pSC = *pLen; FROM_OFF(*pLen); *pDesiredCol = *pCol; ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Ctrl+C -- copy if (key == KEY_CTRL_C) { if (HAS_SEL()) { clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); } return; } if (opts->readOnly) { goto navigation; } // Ctrl+V -- paste if (key == KEY_CTRL_V) { int32_t clipLen = 0; const char *clip = clipboardGet(&clipLen); if (clipLen > 0) { textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); if (HAS_SEL()) { int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); FROM_OFF(lo); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); int32_t canFit = bufSize - 1 - *pLen; int32_t paste = clipLen < canFit ? clipLen : canFit; if (paste > 0) { memmove(buf + off + paste, buf + off, *pLen - off + 1); memcpy(buf + off, clip, paste); *pLen += paste; FROM_OFF(off + paste); *pDesiredCol = *pCol; } if (w->onChange) { w->onChange(w); } textEditLineCacheDirty(lc); } ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Ctrl+X -- cut if (key == KEY_CTRL_X) { if (HAS_SEL()) { clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); FROM_OFF(lo); *pSA = -1; *pSC = -1; *pDesiredCol = *pCol; if (w->onChange) { w->onChange(w); } textEditLineCacheDirty(lc); } ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Ctrl+Z -- undo (single-slot swap; gives you one level of redo) if (key == KEY_CTRL_Z) { if (undoBuf && *pUndoLen >= 0) { char tmpBuf[*pLen + 1]; int32_t tmpLen = *pLen; int32_t tmpCursor = CUR_OFF(); 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 saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; memcpy(undoBuf, tmpBuf, saveLen); undoBuf[saveLen] = '\0'; *pUndoLen = saveLen; *pUndoCursor = tmpCursor; int32_t restoreOff = *pUndoCursor < *pLen ? *pUndoCursor : *pLen; *pUndoCursor = tmpCursor; FROM_OFF(restoreOff); *pDesiredCol = *pCol; *pSA = -1; *pSC = -1; if (w->onChange) { w->onChange(w); } textEditLineCacheDirty(lc); } ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Enter -- insert newline (optionally copies leading whitespace) if (key == 0x0D) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); if (HAS_SEL()) { int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); FROM_OFF(lo); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); int32_t indent = 0; char indentBuf[64]; if (opts->autoIndent) { int32_t lineStart = textEditLineStart(lc, buf, *pLen, *pRow); while (lineStart + indent < off && indent < 63 && (buf[lineStart + indent] == ' ' || buf[lineStart + indent] == '\t')) { indentBuf[indent] = buf[lineStart + indent]; indent++; } } if (*pLen + 1 + indent < bufSize) { memmove(buf + off + 1 + indent, buf + off, *pLen - off + 1); buf[off] = '\n'; memcpy(buf + off + 1, indentBuf, indent); *pLen += 1 + indent; (*pRow)++; *pCol = indent; *pDesiredCol = indent; } if (w->onChange) { w->onChange(w); } textEditLineCacheDirty(lc); } ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Backspace if (key == 8) { if (HAS_SEL()) { textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); FROM_OFF(lo); *pSA = -1; *pSC = -1; *pDesiredCol = *pCol; textEditLineCacheDirty(lc); if (w->onChange) { w->onChange(w); } } else { int32_t off = CUR_OFF(); if (off > 0) { char deleted = buf[off - 1]; textEditSaveUndo(buf, *pLen, off, undoBuf, pUndoLen, pUndoCursor, bufSize); memmove(buf + off - 1, buf + off, *pLen - off + 1); (*pLen)--; FROM_OFF(off - 1); *pDesiredCol = *pCol; if (deleted == '\n') { textEditLineCacheDirty(lc); } else { textEditLineCacheNotifyDelete(lc, buf, *pLen, off - 1, 1); } if (w->onChange) { w->onChange(w); } } } ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Delete if (key == KEY_DELETE) { if (HAS_SEL()) { textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); FROM_OFF(lo); *pSA = -1; *pSC = -1; *pDesiredCol = *pCol; textEditLineCacheDirty(lc); if (w->onChange) { w->onChange(w); } } else { int32_t off = CUR_OFF(); if (off < *pLen) { char deleted = buf[off]; textEditSaveUndo(buf, *pLen, off, undoBuf, pUndoLen, pUndoCursor, bufSize); memmove(buf + off, buf + off + 1, *pLen - off); (*pLen)--; if (deleted == '\n') { textEditLineCacheDirty(lc); } else { textEditLineCacheNotifyDelete(lc, buf, *pLen, off, 1); } if (w->onChange) { w->onChange(w); } } } ENSURE_VIS(); wgtInvalidatePaint(w); return; } navigation: { int32_t totalLines = textEditLineCount(lc, buf, *pLen); // Left arrow if (key == KEY_LEFT) { SEL_BEGIN(); int32_t off = CUR_OFF(); if (off > 0) { FROM_OFF(off - 1); } *pDesiredCol = *pCol; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Right arrow if (key == KEY_RIGHT) { SEL_BEGIN(); int32_t off = CUR_OFF(); if (off < *pLen) { FROM_OFF(off + 1); } *pDesiredCol = *pCol; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Ctrl+Left -- word left if (key == (0x73 | 0x100)) { SEL_BEGIN(); int32_t off = CUR_OFF(); int32_t newOff = wordBoundaryLeft(buf, off); FROM_OFF(newOff); *pDesiredCol = *pCol; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Ctrl+Right -- word right if (key == (0x74 | 0x100)) { SEL_BEGIN(); int32_t off = CUR_OFF(); int32_t newOff = wordBoundaryRight(buf, *pLen, off); FROM_OFF(newOff); *pDesiredCol = *pCol; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Up arrow if (key == KEY_UP) { SEL_BEGIN(); if (*pRow > 0) { (*pRow)--; int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow); *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL; } SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Down arrow if (key == KEY_DOWN) { SEL_BEGIN(); if (*pRow < totalLines - 1) { (*pRow)++; int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow); *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL; } SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Home if (key == KEY_HOME) { SEL_BEGIN(); *pCol = 0; *pDesiredCol = 0; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // End if (key == KEY_END) { SEL_BEGIN(); *pCol = textEditLineLen(lc, buf, *pLen, *pRow); *pDesiredCol = *pCol; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Page Up if (key == KEY_PGUP) { SEL_BEGIN(); *pRow -= visRows; if (*pRow < 0) { *pRow = 0; } int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow); *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Page Down if (key == KEY_PGDN) { SEL_BEGIN(); *pRow += visRows; if (*pRow >= totalLines) { *pRow = totalLines - 1; } int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow); *pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Ctrl+Home if (key == (0x77 | 0x100)) { SEL_BEGIN(); *pRow = 0; *pCol = 0; *pDesiredCol = 0; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } // Ctrl+End if (key == (0x75 | 0x100)) { SEL_BEGIN(); FROM_OFF(*pLen); *pDesiredCol = *pCol; SEL_END(); ENSURE_VIS(); wgtInvalidatePaint(w); return; } } // Tab key if (key == 9 && opts->captureTabs && !opts->readOnly) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); if (HAS_SEL()) { int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); FROM_OFF(lo); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); if (opts->useTabChar) { if (*pLen < bufSize - 1) { memmove(buf + off + 1, buf + off, *pLen - off + 1); buf[off] = '\t'; (*pLen)++; (*pCol)++; *pDesiredCol = *pCol; } } else { int32_t spaces = tabW - (*pCol % tabW); for (int32_t s = 0; s < spaces && *pLen < bufSize - 1; s++) { memmove(buf + off + 1, buf + off, *pLen - off + 1); buf[off] = ' '; off++; (*pLen)++; (*pCol)++; } *pDesiredCol = *pCol; } textEditLineCacheDirty(lc); ENSURE_VIS(); wgtInvalidatePaint(w); } return; } // Printable character if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && !opts->readOnly) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize); if (HAS_SEL()) { int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); FROM_OFF(lo); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); if (*pLen < bufSize - 1) { memmove(buf + off + 1, buf + off, *pLen - off + 1); buf[off] = (char)key; (*pLen)++; (*pCol)++; *pDesiredCol = *pCol; textEditLineCacheNotifyInsert(lc, buf, *pLen, off, 1); } else { textEditLineCacheDirty(lc); } if (w->onChange) { w->onChange(w); } } ENSURE_VIS(); wgtInvalidatePaint(w); return; } #undef CUR_OFF #undef ENSURE_VIS #undef FROM_OFF #undef SEL_BEGIN #undef SEL_END #undef HAS_SEL #undef SEL_LO #undef SEL_HI } // Paints the text content of a multi-line editor inside the box at // (textX, textY, innerW, visRows*charHeight). Handles tab expansion, // optional per-character syntax coloring, selection highlighting, // past-EOL selection fill, cursor draw, and optional per-line // background overrides via hooks->lineDecorator. // // Does NOT touch the surrounding border, gutter, scrollbars, or // focus rect -- those are caller concerns (widget chrome). // // Selection offsets are byte offsets into buf. selAnchor/selCursor // may be -1 to indicate no selection; unequal values with both >= 0 // produce a highlighted range. void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks) { int32_t totalLines = textEditLineCount(lc, buf, len); int32_t selLo = -1; int32_t selHi = -1; if (selAnchor >= 0 && selCursor >= 0 && selAnchor != selCursor) { selLo = selAnchor < selCursor ? selAnchor : selCursor; selHi = selAnchor < selCursor ? selCursor : selAnchor; } int32_t lineOff = textEditLineStart(lc, buf, len, scrollRow); int32_t scratchLen = hooks ? hooks->scratchLen : 0; uint8_t *rawSyntax = hooks ? hooks->rawSyntax : NULL; char *expandBuf = hooks ? hooks->expandBuf : NULL; uint8_t *syntaxBuf = hooks ? hooks->syntaxBuf : NULL; for (int32_t i = 0; i < visRows; i++) { int32_t row = scrollRow + i; if (row >= totalLines) { break; } int32_t lineL = textEditLineLen(lc, buf, len, row); int32_t drawY = textY + i * font->charHeight; uint32_t lineBg = bg; uint32_t gutterColor = 0; if (hooks && hooks->lineDecorator) { uint32_t decBg = hooks->lineDecorator(row + 1, &gutterColor, hooks->lineDecoratorCtx); if (decBg) { lineBg = decBg; } rectFill(d, ops, textX, drawY, innerW, font->charHeight, lineBg); } uint32_t savedBg = bg; bg = lineBg; bool hasSyntax = false; if (hooks && hooks->colorize && lineL > 0 && rawSyntax && scratchLen > 0) { int32_t colorLen = lineL < scratchLen ? lineL : scratchLen; memset(rawSyntax, 0, colorLen); hooks->colorize(buf + lineOff, colorLen, rawSyntax, hooks->colorizeCtx); hasSyntax = true; } int32_t expandLen = 0; int32_t vc = 0; if (expandBuf && syntaxBuf && scratchLen > 0) { for (int32_t j = 0; j < lineL && expandLen < scratchLen - tabW; j++) { if (buf[lineOff + j] == '\t') { int32_t spaces = tabW - (vc % tabW); uint8_t sc = hasSyntax && j < scratchLen ? rawSyntax[j] : 0; for (int32_t s = 0; s < spaces && expandLen < scratchLen; s++) { expandBuf[expandLen] = ' '; syntaxBuf[expandLen] = sc; expandLen++; vc++; } } else { expandBuf[expandLen] = buf[lineOff + j]; syntaxBuf[expandLen] = hasSyntax && j < scratchLen ? rawSyntax[j] : 0; expandLen++; vc++; } } } int32_t visStart = scrollCol; int32_t visEnd = scrollCol + visCols; int32_t textEnd = expandLen; int32_t drawStart = visStart < textEnd ? visStart : textEnd; int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd; int32_t lineSelLo = -1; int32_t lineSelHi = -1; if (selLo >= 0) { if (selLo < lineOff + lineL + 1 && selHi > lineOff) { int32_t byteSelLo = selLo - lineOff; int32_t byteSelHi = selHi - lineOff; if (byteSelLo < 0) { byteSelLo = 0; } lineSelLo = textEditVisualCol(buf, lineOff, lineOff + byteSelLo, tabW); lineSelHi = textEditVisualCol(buf, lineOff, lineOff + (byteSelHi < lineL ? byteSelHi : lineL), tabW); if (byteSelHi > lineL) { lineSelHi = expandLen + 1; } } } const uint32_t *custom = hooks ? hooks->customSyntaxColors : NULL; if (lineSelLo >= 0 && lineSelLo < lineSelHi) { int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo; int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd; if (vSelLo > vSelHi) { vSelLo = vSelHi; } if (drawStart < vSelLo) { if (hasSyntax) { textEditDrawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, custom); } else { drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, fg, bg, true); } } if (vSelLo < vSelHi) { drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, expandBuf + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); } if (vSelHi < drawEnd) { if (hasSyntax) { textEditDrawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, custom); } else { drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true); } } int32_t nlOff = lineOff + lineL; bool pastEolSelected = (nlOff >= selLo && nlOff < selHi); if (pastEolSelected && drawEnd < visEnd) { int32_t selPastStart = drawEnd < lineSelLo ? lineSelLo : drawEnd; int32_t selPastEnd = visEnd; if (selPastStart < visStart) { selPastStart = visStart; } if (selPastStart < selPastEnd) { rectFill(d, ops, textX + (selPastStart - scrollCol) * font->charWidth, drawY, (selPastEnd - selPastStart) * font->charWidth, font->charHeight, colors->menuHighlightBg); } } } else { if (drawStart < drawEnd) { if (hasSyntax) { textEditDrawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, custom); } else { drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true); } } } bg = savedBg; lineOff += lineL; if (lineOff < len && buf[lineOff] == '\n') { lineOff++; } } if (showCursor) { int32_t curLineOff = textEditLineStart(lc, buf, len, cursorRow); int32_t curOff = curLineOff + cursorCol; if (curOff > len) { curOff = len; } int32_t curVisCol = textEditVisualCol(buf, curLineOff, curOff, tabW); int32_t curDrawCol = curVisCol - scrollCol; int32_t curDrawRow = cursorRow - scrollRow; if (curDrawCol >= 0 && curDrawCol <= visCols && curDrawRow >= 0 && curDrawRow < visRows) { int32_t cursorX = textX + curDrawCol * font->charWidth; int32_t cursorY = textY + curDrawRow * font->charHeight; drawVLine(d, ops, cursorX, cursorY, font->charHeight, fg); } } } // 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, int32_t fieldWidth) { 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 == KEY_CTRL_C) { if (hasSel) { clipboardCopy(buf + selLo, selHi - selLo); } return; } // Ctrl+V -- paste if (key == KEY_CTRL_V) { 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 == KEY_CTRL_X) { 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 == KEY_CTRL_Z && undoBuf && pUndoLen && pUndoCursor) { // Swap current and undo char tmpBuf[*pLen + 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 >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST) { // 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 == KEY_LEFT) { // Left arrow if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; } if (*pCursor > 0) { (*pCursor)--; } *pSelEnd = *pCursor; } else { if (pSelStart) { *pSelStart = -1; } if (pSelEnd) { *pSelEnd = -1; } if (*pCursor > 0) { (*pCursor)--; } } } else if (key == KEY_RIGHT) { // Right arrow if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *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; } *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; } *pCursor = newPos; *pSelEnd = newPos; } else { if (pSelStart) { *pSelStart = -1; } if (pSelEnd) { *pSelEnd = -1; } *pCursor = newPos; } } else if (key == KEY_HOME) { // Home if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; } *pCursor = 0; *pSelEnd = 0; } else { if (pSelStart) { *pSelStart = -1; } if (pSelEnd) { *pSelEnd = -1; } *pCursor = 0; } } else if (key == KEY_END) { // End if (shift && pSelStart && pSelEnd) { if (*pSelStart < 0) { *pSelStart = *pCursor; } *pCursor = *pLen; *pSelEnd = *pLen; } else { if (pSelStart) { *pSelStart = -1; } if (pSelEnd) { *pSelEnd = -1; } *pCursor = *pLen; } } else if (key == KEY_DELETE) { // 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 = fieldWidth > 0 ? fieldWidth : w->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); } // 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); } } } // Paints a single vertical or horizontal text-grid scrollbar: // trough, end-arrow buttons with direction triangles, and thumb. // All items are in logical units (lines/cols). void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll) { BevelStyleT troughBevel = BEVEL_TROUGH(colors); BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); if (vertical) { drawBevel(d, ops, x, y, thick, len, &troughBevel); } else { drawBevel(d, ops, x, y, len, thick, &troughBevel); } // Starting-end arrow button (up / left) drawBevel(d, ops, x, y, thick, thick, &btnBevel); { int32_t cx = x + thick / 2; int32_t cy = y + thick / 2; if (vertical) { for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg); } } else { for (int32_t i = 0; i < 4; i++) { drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, colors->contentFg); } } } // Ending-end arrow button (down / right) int32_t endX = vertical ? x : x + len - thick; int32_t endY = vertical ? y + len - thick : y; drawBevel(d, ops, endX, endY, thick, thick, &btnBevel); { int32_t cx = endX + thick / 2; int32_t cy = endY + thick / 2; if (vertical) { for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, colors->contentFg); } } else { for (int32_t i = 0; i < 4; i++) { drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, colors->contentFg); } } } int32_t trackLen = len - thick * 2; if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize); if (vertical) { drawBevel(d, ops, x, y + thick + thumbPos, thick, thumbSize, &btnBevel); } else { drawBevel(d, ops, x + thick + thumbPos, y, thumbSize, thick, &btnBevel); } } } // Converts a mouse coordinate (during an active thumb drag) to a // clamped scroll value. dragOff is the thumb-relative offset captured // when the drag started (from the hit-test's *pDragOff). int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff) { (void)vertical; int32_t maxScroll = total - visible; if (maxScroll <= 0) { return 0; } int32_t trackLen = len - thick * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, total, visible, 0, &thumbPos, &thumbSize); int32_t rel = mouseCoord - sbStart - thick - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * rel) / (trackLen - thumbSize) : 0; if (newScroll < 0) { newScroll = 0; } if (newScroll > maxScroll) { newScroll = maxScroll; } return newScroll; } // Classifies a mouse click against a text-grid scrollbar. Returns // one of the TEXT_SB_HIT_* codes. Callers typically translate UP/ // DOWN to single-step scrolls and PAGE_UP/PAGE_DOWN to page-sized // jumps. THUMB begins a drag; *pDragOff captures the click offset // relative to the thumb origin for use in subsequent drag updates. int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff) { int32_t primary = vertical ? (vy - y) : (vx - x); int32_t cross = vertical ? (vx - x) : (vy - y); if (cross < 0 || cross >= thick || primary < 0 || primary >= len) { return TEXT_SB_HIT_NONE; } if (primary < thick) { return TEXT_SB_HIT_UP; } if (primary >= len - thick) { return TEXT_SB_HIT_DOWN; } int32_t trackLen = len - thick * 2; if (trackLen <= 0) { return TEXT_SB_HIT_NONE; } int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize); int32_t trackRel = primary - thick; if (trackRel < thumbPos) { return TEXT_SB_HIT_PAGE_UP; } if (trackRel >= thumbPos + thumbSize) { return TEXT_SB_HIT_PAGE_DOWN; } if (pDragOff) { *pDragOff = trackRel - thumbPos; } return TEXT_SB_HIT_THUMB; } 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; } 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; }