// 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; // 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; 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; } // ============================================================ // 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, 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 == 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 = 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); } // ============================================================ // 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; }