// widgetTextInput.c — TextInput and TextArea widgets #include "widgetInternal.h" #include #include #define TEXTAREA_BORDER 2 #define TEXTAREA_PAD 2 #define TEXTAREA_SB_W 14 #define TEXTAREA_MIN_ROWS 4 #define TEXTAREA_MIN_COLS 20 #define CLIPBOARD_MAX 4096 #define DBLCLICK_TICKS (CLOCKS_PER_SEC / 2) // ============================================================ // Prototypes // ============================================================ static int32_t textAreaCountLines(const char *buf, int32_t len); static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col); static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols); static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row); static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row); static int32_t textAreaMaxLineLen(const char *buf, int32_t len); static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col); 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); // ============================================================ // Shared clipboard // ============================================================ static char sClipboard[CLIPBOARD_MAX]; static int32_t sClipboardLen = 0; void clipboardCopy(const char *text, int32_t len) { if (len > CLIPBOARD_MAX - 1) { len = CLIPBOARD_MAX - 1; } memcpy(sClipboard, text, len); sClipboard[len] = '\0'; sClipboardLen = len; } const char *clipboardGet(int32_t *outLen) { if (outLen) { *outLen = sClipboardLen; } return sClipboard; } // ============================================================ // Multi-click tracking // ============================================================ static clock_t sLastClickTime = 0; static int32_t sLastClickX = -1; static int32_t sLastClickY = -1; static int32_t sClickCount = 0; int32_t multiClickDetect(int32_t vx, int32_t vy) { clock_t now = clock(); if ((now - sLastClickTime) < DBLCLICK_TICKS && abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { sClickCount++; } else { sClickCount = 1; } sLastClickTime = now; sLastClickX = vx; sLastClickY = vy; return sClickCount; } bool isWordChar(char c) { return isalnum((unsigned char)c) || c == '_'; } int32_t wordStart(const char *buf, int32_t pos) { while (pos > 0 && isWordChar(buf[pos - 1])) { pos--; } return pos; } int32_t wordEnd(const char *buf, int32_t len, int32_t pos) { while (pos < len && isWordChar(buf[pos])) { pos++; } return pos; } // ============================================================ // Clear selection on all text widgets except 'except' // ============================================================ static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) { if (!root) { return false; } bool cleared = false; WidgetT *stack[64]; int32_t top = 0; stack[top++] = root; while (top > 0) { WidgetT *w = stack[--top]; if (w != except) { if (w->type == WidgetTextInputE) { if (w->as.textInput.selStart != w->as.textInput.selEnd) { cleared = true; } w->as.textInput.selStart = -1; w->as.textInput.selEnd = -1; } else if (w->type == WidgetTextAreaE) { if (w->as.textArea.selAnchor != w->as.textArea.selCursor) { cleared = true; } w->as.textArea.selAnchor = -1; w->as.textArea.selCursor = -1; } else if (w->type == WidgetComboBoxE) { if (w->as.comboBox.selStart != w->as.comboBox.selEnd) { cleared = true; } w->as.comboBox.selStart = -1; w->as.comboBox.selEnd = -1; } else if (w->type == WidgetAnsiTermE) { if (w->as.ansiTerm.selStartLine >= 0 && (w->as.ansiTerm.selStartLine != w->as.ansiTerm.selEndLine || w->as.ansiTerm.selStartCol != w->as.ansiTerm.selEndCol)) { cleared = true; w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; } w->as.ansiTerm.selStartLine = -1; w->as.ansiTerm.selStartCol = -1; w->as.ansiTerm.selEndLine = -1; w->as.ansiTerm.selEndCol = -1; w->as.ansiTerm.selecting = false; } } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (top < 64) { stack[top++] = c; } } } return cleared; } void clearOtherSelections(WidgetT *except) { if (!except || !except->window || !except->window->widgetRoot) { return; } AppContextT *ctx = (AppContextT *)except->window->widgetRoot->userData; if (!ctx) { return; } for (int32_t i = 0; i < ctx->stack.count; i++) { WindowT *win = ctx->stack.windows[i]; if (win && win->widgetRoot) { if (clearSelectionsInTree(win->widgetRoot, except) && win != except->window) { RectT fullRect = {0, 0, win->contentW, win->contentH}; widgetOnPaint(win, &fullRect); win->contentDirty = true; dvxInvalidateWindow(ctx, win); } } } } // ============================================================ // Shared undo helpers // ============================================================ 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; } // ============================================================ // Shared 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; } // ============================================================ // wgtTextArea // ============================================================ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) { WidgetT *w = widgetAlloc(parent, WidgetTextAreaE); if (w) { int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; w->as.textArea.buf = (char *)malloc(bufSize); w->as.textArea.bufSize = bufSize; if (w->as.textArea.buf) { w->as.textArea.buf[0] = '\0'; } w->as.textArea.undoBuf = (char *)malloc(bufSize); w->as.textArea.selAnchor = -1; w->as.textArea.selCursor = -1; w->as.textArea.desiredCol = 0; w->weight = 100; } return w; } // ============================================================ // wgtTextInput // ============================================================ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { WidgetT *w = widgetAlloc(parent, WidgetTextInputE); if (w) { int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; w->as.textInput.buf = (char *)malloc(bufSize); w->as.textInput.bufSize = bufSize; if (w->as.textInput.buf) { w->as.textInput.buf[0] = '\0'; } w->as.textInput.undoBuf = (char *)malloc(bufSize); w->as.textInput.selStart = -1; w->as.textInput.selEnd = -1; w->weight = 100; } return w; } // ============================================================ // TextArea line helpers // ============================================================ static int32_t textAreaCountLines(const char *buf, int32_t len) { int32_t lines = 1; for (int32_t i = 0; i < len; i++) { if (buf[i] == '\n') { lines++; } } return lines; } static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) { (void)len; int32_t off = 0; for (int32_t r = 0; r < row; r++) { while (off < len && buf[off] != '\n') { off++; } if (off < len) { off++; } } return off; } static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row) { int32_t start = textAreaLineStart(buf, len, row); int32_t end = start; while (end < len && buf[end] != '\n') { end++; } return end - start; } static int32_t textAreaMaxLineLen(const char *buf, int32_t len) { int32_t maxLen = 0; int32_t curLen = 0; for (int32_t i = 0; i < len; i++) { if (buf[i] == '\n') { if (curLen > maxLen) { maxLen = curLen; } curLen = 0; } else { curLen++; } } if (curLen > maxLen) { maxLen = curLen; } return maxLen; } static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col) { int32_t start = textAreaLineStart(buf, len, row); int32_t lineL = textAreaLineLen(buf, len, row); int32_t clampC = col < lineL ? col : lineL; return start + clampC; } static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col) { 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; } static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols) { int32_t row = w->as.textArea.cursorRow; int32_t col = w->as.textArea.cursorCol; if (row < w->as.textArea.scrollRow) { w->as.textArea.scrollRow = row; } if (row >= w->as.textArea.scrollRow + visRows) { w->as.textArea.scrollRow = row - visRows + 1; } if (col < w->as.textArea.scrollCol) { w->as.textArea.scrollCol = col; } if (col >= w->as.textArea.scrollCol + visCols) { w->as.textArea.scrollCol = col - visCols + 1; } } // ============================================================ // widgetTextAreaCalcMinSize // ============================================================ void widgetTextAreaCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = font->charWidth * TEXTAREA_MIN_COLS + TEXTAREA_PAD * 2 + TEXTAREA_BORDER * 2 + TEXTAREA_SB_W; w->calcMinH = font->charHeight * TEXTAREA_MIN_ROWS + TEXTAREA_BORDER * 2; } // ============================================================ // widgetTextAreaDestroy // ============================================================ void widgetTextAreaDestroy(WidgetT *w) { free(w->as.textArea.buf); free(w->as.textArea.undoBuf); } // ============================================================ // widgetTextAreaGetText // ============================================================ const char *widgetTextAreaGetText(const WidgetT *w) { return w->as.textArea.buf ? w->as.textArea.buf : ""; } // ============================================================ // widgetTextAreaOnKey // ============================================================ void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) { if (!w->as.textArea.buf) { return; } clearOtherSelections(w); char *buf = w->as.textArea.buf; int32_t bufSize = w->as.textArea.bufSize; int32_t *pLen = &w->as.textArea.len; int32_t *pRow = &w->as.textArea.cursorRow; int32_t *pCol = &w->as.textArea.cursorCol; int32_t *pSA = &w->as.textArea.selAnchor; int32_t *pSC = &w->as.textArea.selCursor; bool shift = (mod & KEY_MOD_SHIFT) != 0; AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; const BitmapFontT *font = &ctx->font; int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaMaxLineLen(buf, *pLen); bool needHSb = (maxLL > visCols); int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t visRows = innerH / font->charHeight; if (visRows < 1) { visRows = 1; } if (visCols < 1) { visCols = 1; } int32_t totalLines = textAreaCountLines(buf, *pLen); // Helper macros for cursor offset #define CUR_OFF() textAreaCursorToOff(buf, *pLen, *pRow, *pCol) // Start/extend selection #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) // Ctrl+A — select all if (key == 1) { *pSA = 0; *pSC = *pLen; textAreaOffToRowCol(buf, *pLen, pRow, pCol); w->as.textArea.desiredCol = *pCol; textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Ctrl+C — copy if (key == 3) { if (HAS_SEL()) { clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); } return; } // Ctrl+V — paste if (key == 22) { if (sClipboardLen > 0) { textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, 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); textAreaOffToRowCol(buf, lo, pRow, pCol); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); int32_t canFit = bufSize - 1 - *pLen; int32_t paste = sClipboardLen < canFit ? sClipboardLen : canFit; if (paste > 0) { memmove(buf + off + paste, buf + off, *pLen - off + 1); memcpy(buf + off, sClipboard, paste); *pLen += paste; textAreaOffToRowCol(buf, off + paste, pRow, pCol); w->as.textArea.desiredCol = *pCol; } if (w->onChange) { w->onChange(w); } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Ctrl+X — cut if (key == 24) { if (HAS_SEL()) { clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); textAreaOffToRowCol(buf, lo, pRow, pCol); *pSA = -1; *pSC = -1; w->as.textArea.desiredCol = *pCol; if (w->onChange) { w->onChange(w); } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Ctrl+Z — undo if (key == 26) { if (w->as.textArea.undoBuf && w->as.textArea.undoLen >= 0) { // Swap current and undo char tmpBuf[CLIPBOARD_MAX]; 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 = w->as.textArea.undoLen < bufSize - 1 ? w->as.textArea.undoLen : bufSize - 1; memcpy(buf, w->as.textArea.undoBuf, restLen); buf[restLen] = '\0'; *pLen = restLen; // Save current as new undo int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; memcpy(w->as.textArea.undoBuf, tmpBuf, saveLen); w->as.textArea.undoBuf[saveLen] = '\0'; w->as.textArea.undoLen = saveLen; w->as.textArea.undoCursor = tmpCursor; // Restore cursor int32_t restoreOff = w->as.textArea.undoCursor < *pLen ? w->as.textArea.undoCursor : *pLen; w->as.textArea.undoCursor = tmpCursor; textAreaOffToRowCol(buf, restoreOff, pRow, pCol); w->as.textArea.desiredCol = *pCol; *pSA = -1; *pSC = -1; if (w->onChange) { w->onChange(w); } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Enter — insert newline if (key == 0x0D) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, 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); textAreaOffToRowCol(buf, lo, pRow, pCol); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); if (*pLen < bufSize - 1) { memmove(buf + off + 1, buf + off, *pLen - off + 1); buf[off] = '\n'; (*pLen)++; (*pRow)++; *pCol = 0; w->as.textArea.desiredCol = 0; } if (w->onChange) { w->onChange(w); } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Backspace if (key == 8) { if (HAS_SEL()) { textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); textAreaOffToRowCol(buf, lo, pRow, pCol); *pSA = -1; *pSC = -1; w->as.textArea.desiredCol = *pCol; if (w->onChange) { w->onChange(w); } } else { int32_t off = CUR_OFF(); if (off > 0) { textEditSaveUndo(buf, *pLen, off, w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); memmove(buf + off - 1, buf + off, *pLen - off + 1); (*pLen)--; textAreaOffToRowCol(buf, off - 1, pRow, pCol); w->as.textArea.desiredCol = *pCol; if (w->onChange) { w->onChange(w); } } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Delete if (key == (0x53 | 0x100)) { if (HAS_SEL()) { textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); textAreaOffToRowCol(buf, lo, pRow, pCol); *pSA = -1; *pSC = -1; w->as.textArea.desiredCol = *pCol; if (w->onChange) { w->onChange(w); } } else { int32_t off = CUR_OFF(); if (off < *pLen) { textEditSaveUndo(buf, *pLen, off, w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); memmove(buf + off, buf + off + 1, *pLen - off); (*pLen)--; if (w->onChange) { w->onChange(w); } } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Left arrow if (key == (0x4B | 0x100)) { SEL_BEGIN(); int32_t off = CUR_OFF(); if (off > 0) { textAreaOffToRowCol(buf, off - 1, pRow, pCol); } w->as.textArea.desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Right arrow if (key == (0x4D | 0x100)) { SEL_BEGIN(); int32_t off = CUR_OFF(); if (off < *pLen) { textAreaOffToRowCol(buf, off + 1, pRow, pCol); } w->as.textArea.desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Up arrow if (key == (0x48 | 0x100)) { SEL_BEGIN(); if (*pRow > 0) { (*pRow)--; int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); *pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; } SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Down arrow if (key == (0x50 | 0x100)) { SEL_BEGIN(); if (*pRow < totalLines - 1) { (*pRow)++; int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); *pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; } SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Home if (key == (0x47 | 0x100)) { SEL_BEGIN(); *pCol = 0; w->as.textArea.desiredCol = 0; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // End if (key == (0x4F | 0x100)) { SEL_BEGIN(); *pCol = textAreaLineLen(buf, *pLen, *pRow); w->as.textArea.desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Page Up if (key == (0x49 | 0x100)) { SEL_BEGIN(); *pRow -= visRows; if (*pRow < 0) { *pRow = 0; } int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); *pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Page Down if (key == (0x51 | 0x100)) { SEL_BEGIN(); *pRow += visRows; if (*pRow >= totalLines) { *pRow = totalLines - 1; } int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); *pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Ctrl+Home (scancode 0x77) if (key == (0x77 | 0x100)) { SEL_BEGIN(); *pRow = 0; *pCol = 0; w->as.textArea.desiredCol = 0; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Ctrl+End (scancode 0x75) if (key == (0x75 | 0x100)) { SEL_BEGIN(); textAreaOffToRowCol(buf, *pLen, pRow, pCol); w->as.textArea.desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } // Printable character if (key >= 32 && key < 127) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, 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); textAreaOffToRowCol(buf, lo, pRow, pCol); *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)++; w->as.textArea.desiredCol = *pCol; } if (w->onChange) { w->onChange(w); } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidate(w); return; } #undef CUR_OFF #undef SEL_BEGIN #undef SEL_END #undef HAS_SEL #undef SEL_LO #undef SEL_HI } // ============================================================ // widgetTextAreaOnMouse // ============================================================ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { w->focused = true; clearOtherSelections(w); AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t innerY = w->y + TEXTAREA_BORDER; int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len); bool needHSb = (maxLL > visCols); int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t visRows = innerH / font->charHeight; if (visRows < 1) { visRows = 1; } if (visCols < 1) { visCols = 1; } int32_t totalLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len); int32_t maxScroll = totalLines - visRows; if (maxScroll < 0) { maxScroll = 0; } w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); int32_t maxHScroll = maxLL - visCols; if (maxHScroll < 0) { maxHScroll = 0; } w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll); // Check horizontal scrollbar click if (needHSb) { int32_t hsbY = w->y + w->h - TEXTAREA_BORDER - TEXTAREA_SB_W; if (vy >= hsbY && vx < w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W) { int32_t hsbX = w->x + TEXTAREA_BORDER; int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W; int32_t relX = vx - hsbX; int32_t trackLen = hsbW - TEXTAREA_SB_W * 2; if (relX < TEXTAREA_SB_W) { // Left arrow if (w->as.textArea.scrollCol > 0) { w->as.textArea.scrollCol--; } } else if (relX >= hsbW - TEXTAREA_SB_W) { // Right arrow if (w->as.textArea.scrollCol < maxHScroll) { w->as.textArea.scrollCol++; } } else if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, maxLL, visCols, w->as.textArea.scrollCol, &thumbPos, &thumbSize); int32_t trackRelX = relX - TEXTAREA_SB_W; if (trackRelX < thumbPos) { w->as.textArea.scrollCol -= visCols; w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll); } else if (trackRelX >= thumbPos + thumbSize) { w->as.textArea.scrollCol += visCols; w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll); } } return; } } // Check vertical scrollbar click int32_t sbX = w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W; if (vx >= sbX) { int32_t sbY = w->y + TEXTAREA_BORDER; int32_t sbH = innerH; int32_t relY = vy - sbY; int32_t trackLen = sbH - TEXTAREA_SB_W * 2; if (relY < TEXTAREA_SB_W) { // Up arrow if (w->as.textArea.scrollRow > 0) { w->as.textArea.scrollRow--; } } else if (relY >= sbH - TEXTAREA_SB_W) { // Down arrow if (w->as.textArea.scrollRow < maxScroll) { w->as.textArea.scrollRow++; } } else if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalLines, visRows, w->as.textArea.scrollRow, &thumbPos, &thumbSize); int32_t trackRelY = relY - TEXTAREA_SB_W; if (trackRelY < thumbPos) { w->as.textArea.scrollRow -= visRows; w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); } else if (trackRelY >= thumbPos + thumbSize) { w->as.textArea.scrollRow += visRows; w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); } } return; } // Click on text area — place cursor int32_t relX = vx - innerX; int32_t relY = vy - innerY; int32_t clickRow = w->as.textArea.scrollRow + relY / font->charHeight; int32_t clickCol = w->as.textArea.scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= totalLines) { clickRow = totalLines - 1; } if (clickCol < 0) { clickCol = 0; } int32_t lineL = textAreaLineLen(w->as.textArea.buf, w->as.textArea.len, clickRow); if (clickCol > lineL) { clickCol = lineL; } int32_t clicks = multiClickDetect(vx, vy); if (clicks >= 3) { // Triple-click: select entire line int32_t lineStart = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, 0); int32_t lineEnd = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, lineL); w->as.textArea.cursorRow = clickRow; w->as.textArea.cursorCol = lineL; w->as.textArea.desiredCol = lineL; w->as.textArea.selAnchor = lineStart; w->as.textArea.selCursor = lineEnd; sDragTextSelect = NULL; return; } if (clicks == 2 && w->as.textArea.buf) { // Double-click: select word int32_t off = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); int32_t ws = wordStart(w->as.textArea.buf, off); int32_t we = wordEnd(w->as.textArea.buf, w->as.textArea.len, off); int32_t weRow; int32_t weCol; textAreaOffToRowCol(w->as.textArea.buf, we, &weRow, &weCol); w->as.textArea.cursorRow = weRow; w->as.textArea.cursorCol = weCol; w->as.textArea.desiredCol = weCol; w->as.textArea.selAnchor = ws; w->as.textArea.selCursor = we; sDragTextSelect = NULL; return; } // Single click: place cursor + start drag-select w->as.textArea.cursorRow = clickRow; w->as.textArea.cursorCol = clickCol; w->as.textArea.desiredCol = clickCol; int32_t anchorOff = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); w->as.textArea.selAnchor = anchorOff; w->as.textArea.selCursor = anchorOff; sDragTextSelect = w; } // ============================================================ // widgetTextDragUpdate — update selection during mouse drag // ============================================================ void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; if (w->type == WidgetTextInputE) { int32_t leftEdge = w->x + TEXT_INPUT_PAD; int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; int32_t rightEdge = leftEdge + maxChars * font->charWidth; // Auto-scroll when mouse is past edges if (vx < leftEdge && w->as.textInput.scrollOff > 0) { w->as.textInput.scrollOff--; } else if (vx >= rightEdge && w->as.textInput.scrollOff + maxChars < w->as.textInput.len) { w->as.textInput.scrollOff++; } int32_t relX = vx - leftEdge; int32_t charPos = relX / font->charWidth + w->as.textInput.scrollOff; if (charPos < 0) { charPos = 0; } if (charPos > w->as.textInput.len) { charPos = w->as.textInput.len; } w->as.textInput.cursorPos = charPos; w->as.textInput.selEnd = charPos; } else if (w->type == WidgetTextAreaE) { int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t innerY = w->y + TEXTAREA_BORDER; int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len); bool needHSb = (maxLL > visCols); int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t visRows = innerH / font->charHeight; int32_t totalLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len); if (visRows < 1) { visRows = 1; } if (visCols < 1) { visCols = 1; } // Auto-scroll vertically if (vy < innerY && w->as.textArea.scrollRow > 0) { w->as.textArea.scrollRow--; } else if (vy >= innerY + visRows * font->charHeight && w->as.textArea.scrollRow + visRows < totalLines) { w->as.textArea.scrollRow++; } // Auto-scroll horizontally int32_t rightEdge = innerX + visCols * font->charWidth; if (vx < innerX && w->as.textArea.scrollCol > 0) { w->as.textArea.scrollCol--; } else if (vx >= rightEdge && w->as.textArea.scrollCol < maxLL - visCols) { w->as.textArea.scrollCol++; } int32_t relX = vx - innerX; int32_t relY = vy - innerY; int32_t clickRow = w->as.textArea.scrollRow + relY / font->charHeight; int32_t clickCol = w->as.textArea.scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= totalLines) { clickRow = totalLines - 1; } if (clickCol < 0) { clickCol = 0; } int32_t lineL = textAreaLineLen(w->as.textArea.buf, w->as.textArea.len, clickRow); if (clickCol > lineL) { clickCol = lineL; } w->as.textArea.cursorRow = clickRow; w->as.textArea.cursorCol = clickCol; w->as.textArea.desiredCol = clickCol; w->as.textArea.selCursor = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); } else if (w->type == WidgetComboBoxE) { int32_t leftEdge = w->x + TEXT_INPUT_PAD; int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth; int32_t rightEdge = leftEdge + maxChars * font->charWidth; // Auto-scroll when mouse is past edges if (vx < leftEdge && w->as.comboBox.scrollOff > 0) { w->as.comboBox.scrollOff--; } else if (vx >= rightEdge && w->as.comboBox.scrollOff + maxChars < w->as.comboBox.len) { w->as.comboBox.scrollOff++; } int32_t relX = vx - leftEdge; int32_t charPos = relX / font->charWidth + w->as.comboBox.scrollOff; if (charPos < 0) { charPos = 0; } if (charPos > w->as.comboBox.len) { charPos = w->as.comboBox.len; } w->as.comboBox.cursorPos = charPos; w->as.comboBox.selEnd = charPos; } else if (w->type == WidgetAnsiTermE) { int32_t baseX = w->x + 2; // ANSI_BORDER int32_t baseY = w->y + 2; int32_t cols = w->as.ansiTerm.cols; int32_t rows = w->as.ansiTerm.rows; int32_t clickRow = (vy - baseY) / font->charHeight; int32_t clickCol = (vx - baseX) / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= rows) { clickRow = rows - 1; } if (clickCol < 0) { clickCol = 0; } if (clickCol >= cols) { clickCol = cols; } w->as.ansiTerm.selEndLine = w->as.ansiTerm.scrollPos + clickRow; w->as.ansiTerm.selEndCol = clickCol; w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; } } // ============================================================ // widgetTextAreaPaint // ============================================================ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; char *buf = w->as.textArea.buf; int32_t len = w->as.textArea.len; int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaMaxLineLen(buf, len); bool needHSb = (maxLL > visCols); int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); int32_t visRows = innerH / font->charHeight; int32_t totalLines = textAreaCountLines(buf, len); bool needVSb = (totalLines > visRows); // Sunken border BevelStyleT bevel = BEVEL_SUNKEN(colors, bg, 2); drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Clamp vertical scroll int32_t maxScroll = totalLines - visRows; if (maxScroll < 0) { maxScroll = 0; } w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); // Clamp horizontal scroll int32_t maxHScroll = maxLL - visCols; if (maxHScroll < 0) { maxHScroll = 0; } w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll); // Selection range int32_t selLo = -1; int32_t selHi = -1; if (w->as.textArea.selAnchor >= 0 && w->as.textArea.selCursor >= 0 && w->as.textArea.selAnchor != w->as.textArea.selCursor) { selLo = w->as.textArea.selAnchor < w->as.textArea.selCursor ? w->as.textArea.selAnchor : w->as.textArea.selCursor; selHi = w->as.textArea.selAnchor < w->as.textArea.selCursor ? w->as.textArea.selCursor : w->as.textArea.selAnchor; } // Draw lines int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t textY = w->y + TEXTAREA_BORDER; for (int32_t i = 0; i < visRows; i++) { int32_t row = w->as.textArea.scrollRow + i; if (row >= totalLines) { break; } int32_t lineOff = textAreaLineStart(buf, len, row); int32_t lineL = textAreaLineLen(buf, len, row); int32_t drawY = textY + i * font->charHeight; for (int32_t j = 0; j < visCols; j++) { int32_t col = w->as.textArea.scrollCol + j; int32_t charOff = lineOff + col; int32_t drawX = textX + j * font->charWidth; uint32_t cfgc = fg; uint32_t cbgc = bg; // Check selection — past end of line, test the newline byte // instead of lineOff+col (which aliases into subsequent lines) bool inSel = false; if (selLo >= 0) { if (col < lineL) { inSel = (charOff >= selLo && charOff < selHi); } else { int32_t nlOff = lineOff + lineL; inSel = (nlOff >= selLo && nlOff < selHi); } } if (inSel) { cfgc = colors->menuHighlightFg; cbgc = colors->menuHighlightBg; } if (col < lineL) { drawChar(d, ops, font, drawX, drawY, buf[charOff], cfgc, cbgc, true); } else if (inSel) { rectFill(d, ops, drawX, drawY, font->charWidth, font->charHeight, cbgc); } } } // Draw cursor if (w->focused) { int32_t curDrawCol = w->as.textArea.cursorCol - w->as.textArea.scrollCol; int32_t curDrawRow = w->as.textArea.cursorRow - w->as.textArea.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); } } BevelStyleT btnBevel = BEVEL_RAISED(colors, 1); // Draw vertical scrollbar if (needVSb) { int32_t sbX = w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W; int32_t sbY = w->y + TEXTAREA_BORDER; int32_t sbH = innerH; // Trough BevelStyleT troughBevel = BEVEL_TROUGH(colors); drawBevel(d, ops, sbX, sbY, TEXTAREA_SB_W, sbH, &troughBevel); // Up arrow button drawBevel(d, ops, sbX, sbY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel); // Up arrow triangle { int32_t cx = sbX + TEXTAREA_SB_W / 2; int32_t cy = sbY + TEXTAREA_SB_W / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg); } } // Down arrow button int32_t downY = sbY + sbH - TEXTAREA_SB_W; drawBevel(d, ops, sbX, downY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel); // Down arrow triangle { int32_t cx = sbX + TEXTAREA_SB_W / 2; int32_t cy = downY + TEXTAREA_SB_W / 2; for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, colors->contentFg); } } // Thumb int32_t trackLen = sbH - TEXTAREA_SB_W * 2; if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalLines, visRows, w->as.textArea.scrollRow, &thumbPos, &thumbSize); drawBevel(d, ops, sbX, sbY + TEXTAREA_SB_W + thumbPos, TEXTAREA_SB_W, thumbSize, &btnBevel); } } // Draw horizontal scrollbar if (needHSb) { int32_t hsbX = w->x + TEXTAREA_BORDER; int32_t hsbY = w->y + w->h - TEXTAREA_BORDER - TEXTAREA_SB_W; int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W; // Trough BevelStyleT troughBevel = BEVEL_TROUGH(colors); drawBevel(d, ops, hsbX, hsbY, hsbW, TEXTAREA_SB_W, &troughBevel); // Left arrow button drawBevel(d, ops, hsbX, hsbY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel); // Left arrow triangle { int32_t cx = hsbX + TEXTAREA_SB_W / 2; int32_t cy = hsbY + TEXTAREA_SB_W / 2; for (int32_t i = 0; i < 4; i++) { drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, colors->contentFg); } } // Right arrow button int32_t rightX = hsbX + hsbW - TEXTAREA_SB_W; drawBevel(d, ops, rightX, hsbY, TEXTAREA_SB_W, TEXTAREA_SB_W, &btnBevel); // Right arrow triangle { int32_t cx = rightX + TEXTAREA_SB_W / 2; int32_t cy = hsbY + TEXTAREA_SB_W / 2; for (int32_t i = 0; i < 4; i++) { drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, colors->contentFg); } } // Thumb int32_t trackLen = hsbW - TEXTAREA_SB_W * 2; if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, maxLL, visCols, w->as.textArea.scrollCol, &thumbPos, &thumbSize); drawBevel(d, ops, hsbX + TEXTAREA_SB_W + thumbPos, hsbY, thumbSize, TEXTAREA_SB_W, &btnBevel); } // Dead corner between scrollbars if (needVSb) { int32_t cornerX = w->x + w->w - TEXTAREA_BORDER - TEXTAREA_SB_W; rectFill(d, ops, cornerX, hsbY, TEXTAREA_SB_W, TEXTAREA_SB_W, colors->windowFace); } } // Focus rect if (w->focused) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); } } // ============================================================ // widgetTextAreaSetText // ============================================================ void widgetTextAreaSetText(WidgetT *w, const char *text) { if (w->as.textArea.buf) { strncpy(w->as.textArea.buf, text, w->as.textArea.bufSize - 1); w->as.textArea.buf[w->as.textArea.bufSize - 1] = '\0'; w->as.textArea.len = (int32_t)strlen(w->as.textArea.buf); w->as.textArea.cursorRow = 0; w->as.textArea.cursorCol = 0; w->as.textArea.scrollRow = 0; w->as.textArea.scrollCol = 0; w->as.textArea.desiredCol = 0; w->as.textArea.selAnchor = -1; w->as.textArea.selCursor = -1; } } // ============================================================ // widgetTextInputCalcMinSize // ============================================================ void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font) { w->calcMinW = font->charWidth * 8 + TEXT_INPUT_PAD * 2; w->calcMinH = font->charHeight + TEXT_INPUT_PAD * 2; } // ============================================================ // widgetTextInputDestroy // ============================================================ void widgetTextInputDestroy(WidgetT *w) { free(w->as.textInput.buf); free(w->as.textInput.undoBuf); } // ============================================================ // widgetTextInputGetText // ============================================================ const char *widgetTextInputGetText(const WidgetT *w) { return w->as.textInput.buf ? w->as.textInput.buf : ""; } // ============================================================ // widgetTextInputOnKey // ============================================================ void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) { if (!w->as.textInput.buf) { return; } clearOtherSelections(w); widgetTextEditOnKey(w, key, mod, w->as.textInput.buf, w->as.textInput.bufSize, &w->as.textInput.len, &w->as.textInput.cursorPos, &w->as.textInput.scrollOff, &w->as.textInput.selStart, &w->as.textInput.selEnd, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor); } // ============================================================ // widgetTextInputOnMouse // ============================================================ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { w->focused = true; clearOtherSelections(w); AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t relX = vx - w->x - TEXT_INPUT_PAD; int32_t charPos = relX / font->charWidth + w->as.textInput.scrollOff; if (charPos < 0) { charPos = 0; } if (charPos > w->as.textInput.len) { charPos = w->as.textInput.len; } int32_t clicks = multiClickDetect(vx, vy); if (clicks >= 3) { // Triple-click: select all (single line) w->as.textInput.selStart = 0; w->as.textInput.selEnd = w->as.textInput.len; w->as.textInput.cursorPos = w->as.textInput.len; sDragTextSelect = NULL; return; } if (clicks == 2 && w->as.textInput.buf) { // Double-click: select word int32_t ws = wordStart(w->as.textInput.buf, charPos); int32_t we = wordEnd(w->as.textInput.buf, w->as.textInput.len, charPos); w->as.textInput.selStart = ws; w->as.textInput.selEnd = we; w->as.textInput.cursorPos = we; sDragTextSelect = NULL; return; } // Single click: place cursor + start drag-select w->as.textInput.cursorPos = charPos; w->as.textInput.selStart = charPos; w->as.textInput.selEnd = charPos; sDragTextSelect = w; } // ============================================================ // widgetTextInputPaint // ============================================================ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; // Sunken border BevelStyleT bevel; bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; bevel.face = bg; bevel.width = 2; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); // Draw text if (w->as.textInput.buf) { int32_t textX = w->x + TEXT_INPUT_PAD; int32_t textY = w->y + (w->h - font->charHeight) / 2; int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; int32_t off = w->as.textInput.scrollOff; int32_t len = w->as.textInput.len - off; if (len > maxChars) { len = maxChars; } // Selection range int32_t selLo = -1; int32_t selHi = -1; if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) { selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd; selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart; } for (int32_t i = 0; i < len; i++) { int32_t charIdx = off + i; uint32_t cfgc = fg; uint32_t cbgc = bg; if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) { cfgc = colors->menuHighlightFg; cbgc = colors->menuHighlightBg; } drawChar(d, ops, font, textX + i * font->charWidth, textY, w->as.textInput.buf[charIdx], cfgc, cbgc, true); } // Draw cursor if (w->focused) { int32_t cursorX = textX + (w->as.textInput.cursorPos - off) * font->charWidth; if (cursorX >= w->x + TEXT_INPUT_PAD && cursorX < w->x + w->w - TEXT_INPUT_PAD) { drawVLine(d, ops, cursorX, textY, font->charHeight, fg); } } } } // ============================================================ // widgetTextInputSetText // ============================================================ void widgetTextInputSetText(WidgetT *w, const char *text) { if (w->as.textInput.buf) { strncpy(w->as.textInput.buf, text, w->as.textInput.bufSize - 1); w->as.textInput.buf[w->as.textInput.bufSize - 1] = '\0'; w->as.textInput.len = (int32_t)strlen(w->as.textInput.buf); w->as.textInput.cursorPos = w->as.textInput.len; w->as.textInput.scrollOff = 0; w->as.textInput.selStart = -1; w->as.textInput.selEnd = -1; } } // ============================================================ // widgetTextEditOnKey — shared single-line text editing logic // ============================================================ 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; // 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) { if (sClipboardLen > 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 < sClipboardLen && paste < canFit; i++) { if (sClipboard[i] != '\n' && sClipboard[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 < sClipboardLen && j < paste; i++) { if (sClipboard[i] != '\n' && sClipboard[i] != '\r') { buf[pos + j] = sClipboard[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[CLIPBOARD_MAX]; 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 == (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 = (AppContextT *)w->window->widgetRoot->userData; const BitmapFontT *font = &ctx->font; int32_t fieldW = w->w; if (w->type == WidgetComboBoxE) { fieldW -= DROPDOWN_BTN_WIDTH; } int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth; if (*pCursor < *pScrollOff) { *pScrollOff = *pCursor; } if (*pCursor >= *pScrollOff + visibleChars) { *pScrollOff = *pCursor - visibleChars + 1; } } wgtInvalidate(w); }