#define DVX_WIDGET_IMPL // widgetTextInput.c -- TextInput and TextArea widgets // // This file implements two text editing widgets: // // 1. TextInput -- single-line text field with scroll, selection, // undo, password masking, and masked input (e.g., phone/SSN). // 2. TextArea -- multi-line text editor with row/col cursor, // dual-axis scrolling, and full selection/clipboard support. // // Shared infrastructure (clipboard, multi-click detection, word // boundary logic, cross-widget selection clearing, and the // single-line editing engine widgetTextEditOnKey) lives in // widgetCore.c so it can be linked from any widget DXE. // // All text editing is done in-place in fixed-size char buffers // allocated at widget creation. No dynamic resizing -- this keeps // memory management simple and predictable on DOS where heap // fragmentation is a real concern. // // Undo is single-level swap: before each mutation, the current // buffer is copied to undoBuf. Ctrl+Z swaps current<->undo, so // a second Ctrl+Z is "redo". This is simpler than a multi-level // undo stack and sufficient for typical DOS text entry. // // Selection model: selStart/selEnd (single-line) or selAnchor/ // selCursor (multi-line) form a directed range. selStart is where // the user began selecting, selEnd is where they stopped. The // "low" end for deletion/copy is always min(start, end). The // -1 sentinel means "no selection". // // Clipboard is a simple static buffer (4KB). This is a process-wide // clipboard, not per-widget and not OS-integrated (DOS has no // clipboard API). Text cut/copied from any widget is available to // paste in any other widget. // // Multi-click detection uses clock() timestamps with a 500ms // threshold and 4px spatial tolerance. Double-click selects word, // triple-click selects line (TextArea) or all (TextInput). // // Cross-widget selection clearing: when a widget gains selection, // clearOtherSelections() deselects any other widget that had an // active selection. This prevents the confusing visual state of // multiple selected ranges across different widgets. The tracking // uses sLastSelectedWidget to achieve O(1) clearing rather than // walking the entire widget tree. // // Masked input: a special TextInput mode where the buffer is // pre-filled from a mask pattern (e.g., "###-##-####" for SSN). // '#' accepts digits, 'A' accepts letters, '*' accepts any // printable char. Literal characters in the mask are fixed and the // cursor skips over them. This provides constrained input without // needing a separate widget type. // // Password mode: renders bullets instead of characters (CP437 0xF9) // and blocks copy/cut operations for security. #include "dvxWgtP.h" #include "../texthelp/textHelp.h" static int32_t sTextInputTypeId = -1; static int32_t sTextAreaTypeId = -1; typedef enum { InputNormalE, InputPasswordE, InputMaskedE } InputModeE; typedef struct { char *buf; int32_t bufSize; int32_t len; int32_t cursorPos; int32_t scrollOff; int32_t selStart; int32_t selEnd; char *undoBuf; int32_t undoLen; int32_t undoCursor; InputModeE inputMode; const char *mask; } TextInputDataT; typedef struct { char *buf; int32_t bufSize; int32_t len; int32_t cursorRow; int32_t cursorCol; int32_t scrollRow; int32_t scrollCol; int32_t desiredCol; int32_t selAnchor; int32_t selCursor; char *undoBuf; int32_t undoLen; int32_t undoCursor; int32_t cachedLines; int32_t cachedMaxLL; // Line offset cache: lineOffsets[i] = byte offset of start of line i. // lineOffsets[lineCount] = past-end sentinel (len or len+1). // Rebuilt lazily when cachedLines == -1. int32_t *lineOffsets; int32_t lineOffsetCap; // Per-line visual length cache (tab-expanded). -1 = dirty. int32_t *lineVisLens; int32_t lineVisLenCap; // Cached cursor byte offset (avoids O(N) recomputation per keystroke) int32_t cursorOff; int32_t sbDragOrient; int32_t sbDragOff; bool sbDragging; bool showLineNumbers; bool autoIndent; bool captureTabs; // true = Tab key inserts tab/spaces; false = Tab moves focus bool useTabChar; // true = insert '\t'; false = insert spaces int32_t tabWidth; // display width and space count (default 3) // Syntax colorizer callback (optional). Called for each visible line. // line: text of the line (NOT null-terminated, use lineLen). // colors: output array of color indices (0=default, 1-7=syntax colors). // The callback fills colors[0..lineLen-1]. void (*colorize)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); void *colorizeCtx; // Line decorator callback (optional). Called for each visible line during paint. // Returns background color override (0 = use default). Sets *gutterColor to a // non-zero color to draw a filled circle in the gutter for breakpoints. uint32_t (*lineDecorator)(int32_t lineNum, uint32_t *gutterColor, void *ctx); void *lineDecoratorCtx; // Gutter click callback (optional). Fired when user clicks in the gutter. void (*onGutterClick)(WidgetT *w, int32_t lineNum); // Pre-allocated paint buffers (avoid 3KB stack alloc per visible line per frame) uint8_t *rawSyntax; // syntax color buffer (MAX_COLORIZE_LEN) char *expandBuf; // tab-expanded text (MAX_COLORIZE_LEN) uint8_t *syntaxBuf; // tab-expanded syntax colors (MAX_COLORIZE_LEN) // Cached gutter width (recomputed only when line count changes) int32_t cachedGutterW; int32_t cachedGutterLines; // line count when cachedGutterW was computed } TextAreaDataT; #include #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 MAX_COLORIZE_LEN 1024 // Match the ANSI terminal cursor blink rate (CURSOR_MS in widgetAnsiTerm.c) #define CURSOR_BLINK_MS 250 // Syntax color indices (returned by colorize callback) #define SYNTAX_DEFAULT 0 #define SYNTAX_KEYWORD 1 #define SYNTAX_STRING 2 #define SYNTAX_COMMENT 3 #define SYNTAX_NUMBER 4 #define SYNTAX_OPERATOR 5 #define SYNTAX_TYPE 6 #define SYNTAX_MAX 7 // ============================================================ // Prototypes // ============================================================ static void drawColorizedText(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 ColorSchemeT *colors); static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors); static bool maskCharValid(char slot, char ch); static int32_t maskFirstSlot(const char *mask); static bool maskIsSlot(char ch); static int32_t maskNextSlot(const char *mask, int32_t pos); static int32_t maskPrevSlot(const char *mask, int32_t pos); static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod); static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col); static inline void textAreaDirtyCache(WidgetT *w); static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols); static int32_t textAreaGetLineCount(WidgetT *w); static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font); static int32_t textAreaGetMaxLineLen(WidgetT *w); static int32_t textAreaLineLenCached(WidgetT *w, int32_t row); 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 textAreaLineStartCached(WidgetT *w, int32_t row); static void textAreaRebuildCache(WidgetT *w); static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col); static void textAreaOffToRowColFast(TextAreaDataT *ta, int32_t off, int32_t *row, int32_t *col); static int32_t visualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW); static int32_t visualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW); static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); static void widgetTextAreaOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static int32_t wordBoundaryLeft(const char *buf, int32_t pos); static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); // sCursorBlinkOn is defined in widgetCore.c (shared state) // ============================================================ // 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; } // ============================================================ // wordBoundaryLeft // ============================================================ // // From position pos, skip non-word chars left, then skip word chars left. // Returns the position at the start of the word (or 0). 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; } // ============================================================ // wordBoundaryRight // ============================================================ // // From position pos, skip word chars right, then skip non-word chars right. // Returns the position at the end of the word (or len). 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; } // ============================================================ // Mask helpers // ============================================================ static bool maskIsSlot(char ch) { return ch == '#' || ch == 'A' || ch == '*'; } static bool maskCharValid(char slot, char ch) { switch (slot) { case '#': return ch >= '0' && ch <= '9'; case 'A': return isalpha((unsigned char)ch); case '*': return ch >= 32 && ch < 127; default: return false; } } static int32_t maskFirstSlot(const char *mask) { for (int32_t i = 0; mask[i]; i++) { if (maskIsSlot(mask[i])) { return i; } } return 0; } static int32_t maskNextSlot(const char *mask, int32_t pos) { for (int32_t i = pos + 1; mask[i]; i++) { if (maskIsSlot(mask[i])) { return i; } } return (int32_t)strlen(mask); } static int32_t maskPrevSlot(const char *mask, int32_t pos) { for (int32_t i = pos - 1; i >= 0; i--) { if (maskIsSlot(mask[i])) { return i; } } return pos; } // Masked input key handling is entirely separate from the shared // text editor because the editing semantics are fundamentally different: // the buffer length is fixed (it's always maskLen), characters can // only be placed in slot positions, and backspace clears a slot rather // than removing a character. The cursor skips over literal characters // when moving left/right. Cut clears slots to '_' rather than removing // text. Paste fills consecutive slots, skipping non-matching clipboard // characters. static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) { TextInputDataT *ti = (TextInputDataT *)w->data; char *buf = ti->buf; const char *mask = ti->mask; int32_t *pCur = &ti->cursorPos; int32_t maskLen = ti->len; bool shift = (mod & KEY_MOD_SHIFT) != 0; (void)shift; // Ctrl+A -- select all if (key == 1) { ti->selStart = 0; ti->selEnd = maskLen; *pCur = maskLen; goto done; } // Ctrl+C -- copy formatted text if (key == 3) { if (ti->selStart >= 0 && ti->selEnd >= 0 && ti->selStart != ti->selEnd) { int32_t selLo = ti->selStart < ti->selEnd ? ti->selStart : ti->selEnd; int32_t selHi = ti->selStart < ti->selEnd ? ti->selEnd : ti->selStart; if (selLo < 0) { selLo = 0; } if (selHi > maskLen) { selHi = maskLen; } if (selHi > selLo) { clipboardCopy(buf + selLo, selHi - selLo); } } return; } // Ctrl+V -- paste valid chars into slots if (key == 22) { int32_t clipLen; const char *clip = clipboardGet(&clipLen); if (clipLen > 0) { if (ti->undoBuf) { textEditSaveUndo(buf, maskLen, *pCur, ti->undoBuf, &ti->undoLen, &ti->undoCursor, ti->bufSize); } int32_t slotPos = *pCur; bool changed = false; for (int32_t i = 0; i < clipLen && slotPos < maskLen; i++) { // Skip to next slot if not on one while (slotPos < maskLen && !maskIsSlot(mask[slotPos])) { slotPos++; } if (slotPos >= maskLen) { break; } // Skip non-matching clipboard chars if (maskCharValid(mask[slotPos], clip[i])) { buf[slotPos] = clip[i]; slotPos = maskNextSlot(mask, slotPos); changed = true; } } if (changed) { *pCur = slotPos <= maskLen ? slotPos : maskLen; ti->selStart = -1; ti->selEnd = -1; if (w->onChange) { w->onChange(w); } } } goto done; } // Ctrl+X -- copy and clear selected slots if (key == 24) { if (ti->selStart >= 0 && ti->selEnd >= 0 && ti->selStart != ti->selEnd) { int32_t selLo = ti->selStart < ti->selEnd ? ti->selStart : ti->selEnd; int32_t selHi = ti->selStart < ti->selEnd ? ti->selEnd : ti->selStart; if (selLo < 0) { selLo = 0; } if (selHi > maskLen) { selHi = maskLen; } clipboardCopy(buf + selLo, selHi - selLo); if (ti->undoBuf) { textEditSaveUndo(buf, maskLen, *pCur, ti->undoBuf, &ti->undoLen, &ti->undoCursor, ti->bufSize); } for (int32_t i = selLo; i < selHi; i++) { if (maskIsSlot(mask[i])) { buf[i] = '_'; } } *pCur = selLo; ti->selStart = -1; ti->selEnd = -1; if (w->onChange) { w->onChange(w); } } goto done; } // Ctrl+Z -- undo if (key == 26 && ti->undoBuf) { char tmpBuf[256]; int32_t tmpLen = maskLen + 1 < (int32_t)sizeof(tmpBuf) ? maskLen + 1 : (int32_t)sizeof(tmpBuf); int32_t tmpCursor = *pCur; memcpy(tmpBuf, buf, tmpLen); memcpy(buf, ti->undoBuf, maskLen + 1); *pCur = ti->undoCursor < maskLen ? ti->undoCursor : maskLen; memcpy(ti->undoBuf, tmpBuf, tmpLen); ti->undoCursor = tmpCursor; ti->selStart = -1; ti->selEnd = -1; if (w->onChange) { w->onChange(w); } goto done; } if (key >= 32 && key < 127) { // Printable character -- place at current slot if valid if (*pCur < maskLen && maskIsSlot(mask[*pCur]) && maskCharValid(mask[*pCur], (char)key)) { if (ti->undoBuf) { textEditSaveUndo(buf, maskLen, *pCur, ti->undoBuf, &ti->undoLen, &ti->undoCursor, ti->bufSize); } buf[*pCur] = (char)key; *pCur = maskNextSlot(mask, *pCur); ti->selStart = -1; ti->selEnd = -1; if (w->onChange) { w->onChange(w); } } } else if (key == 8) { // Backspace -- clear previous slot int32_t prev = maskPrevSlot(mask, *pCur); if (prev != *pCur) { if (ti->undoBuf) { textEditSaveUndo(buf, maskLen, *pCur, ti->undoBuf, &ti->undoLen, &ti->undoCursor, ti->bufSize); } buf[prev] = '_'; *pCur = prev; ti->selStart = -1; ti->selEnd = -1; if (w->onChange) { w->onChange(w); } } } else if (key == (0x53 | 0x100)) { // Delete -- clear current slot if (*pCur < maskLen && maskIsSlot(mask[*pCur])) { if (ti->undoBuf) { textEditSaveUndo(buf, maskLen, *pCur, ti->undoBuf, &ti->undoLen, &ti->undoCursor, ti->bufSize); } buf[*pCur] = '_'; ti->selStart = -1; ti->selEnd = -1; if (w->onChange) { w->onChange(w); } } } else if (key == (0x4B | 0x100)) { // Left arrow -- move to previous slot int32_t prev = maskPrevSlot(mask, *pCur); if (shift) { if (ti->selStart < 0) { ti->selStart = *pCur; ti->selEnd = *pCur; } *pCur = prev; ti->selEnd = *pCur; } else { ti->selStart = -1; ti->selEnd = -1; *pCur = prev; } } else if (key == (0x4D | 0x100)) { // Right arrow -- move to next slot int32_t next = maskNextSlot(mask, *pCur); if (shift) { if (ti->selStart < 0) { ti->selStart = *pCur; ti->selEnd = *pCur; } *pCur = next; ti->selEnd = *pCur; } else { ti->selStart = -1; ti->selEnd = -1; *pCur = next; } } else if (key == (0x47 | 0x100)) { // Home -- first slot if (shift) { if (ti->selStart < 0) { ti->selStart = *pCur; ti->selEnd = *pCur; } *pCur = maskFirstSlot(mask); ti->selEnd = *pCur; } else { ti->selStart = -1; ti->selEnd = -1; *pCur = maskFirstSlot(mask); } } else if (key == (0x4F | 0x100)) { // End -- past last slot int32_t last = maskLen; if (shift) { if (ti->selStart < 0) { ti->selStart = *pCur; ti->selEnd = *pCur; } *pCur = last; ti->selEnd = *pCur; } else { ti->selStart = -1; ti->selEnd = -1; *pCur = last; } } else { return; } done: // Adjust scroll { AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t visibleChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; if (*pCur < ti->scrollOff) { ti->scrollOff = *pCur; } if (*pCur >= ti->scrollOff + visibleChars) { ti->scrollOff = *pCur - visibleChars + 1; } } wgtInvalidatePaint(w); } // ============================================================ // TextArea line helpers // ============================================================ // ============================================================ // Line offset cache // ============================================================ // // lineOffsets[i] = byte offset where line i starts. // lineOffsets[lineCount] = len (sentinel past last line). // Built lazily on first access after invalidation (cachedLines == -1). // Single O(N) scan builds both the line offset table and per-line // visual length cache simultaneously. static void textAreaRebuildCache(WidgetT *w) { TextAreaDataT *ta = (TextAreaDataT *)w->data; char *buf = ta->buf; int32_t len = ta->len; int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3; // Count lines first int32_t lineCount = 1; for (int32_t i = 0; i < len; i++) { if (buf[i] == '\n') { lineCount++; } } // Grow line offset array if needed (+1 for sentinel) int32_t needed = lineCount + 1; if (needed > ta->lineOffsetCap) { int32_t newCap = needed + 256; ta->lineOffsets = (int32_t *)realloc(ta->lineOffsets, newCap * sizeof(int32_t)); ta->lineOffsetCap = newCap; } // Grow visual length array if needed if (lineCount > ta->lineVisLenCap) { int32_t newCap = lineCount + 256; ta->lineVisLens = (int32_t *)realloc(ta->lineVisLens, newCap * sizeof(int32_t)); ta->lineVisLenCap = newCap; } // Single pass: record offsets and compute visual lengths int32_t line = 0; int32_t vc = 0; int32_t maxVL = 0; ta->lineOffsets[0] = 0; for (int32_t i = 0; i < len; i++) { if (buf[i] == '\n') { ta->lineVisLens[line] = vc; if (vc > maxVL) { maxVL = vc; } line++; ta->lineOffsets[line] = i + 1; vc = 0; } else if (buf[i] == '\t') { vc += tabW - (vc % tabW); } else { vc++; } } // Last line (may not end with newline) ta->lineVisLens[line] = vc; if (vc > maxVL) { maxVL = vc; } ta->lineOffsets[lineCount] = len; ta->cachedLines = lineCount; ta->cachedMaxLL = maxVL; } static void textAreaEnsureCache(WidgetT *w) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->cachedLines < 0) { textAreaRebuildCache(w); } } static int32_t textAreaGetLineCount(WidgetT *w) { textAreaEnsureCache(w); return ((TextAreaDataT *)w->data)->cachedLines; } static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (!ta->showLineNumbers) { return 0; } int32_t totalLines = textAreaGetLineCount(w); // Return cached value if line count hasn't changed if (ta->cachedGutterW > 0 && ta->cachedGutterLines == totalLines) { return ta->cachedGutterW; } int32_t digits = 1; int32_t temp = totalLines; while (temp >= 10) { temp /= 10; digits++; } if (digits < 3) { digits = 3; } ta->cachedGutterW = (digits + 1) * font->charWidth; ta->cachedGutterLines = totalLines; return ta->cachedGutterW; } static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) { (void)buf; (void)len; (void)row; // Should not be called — use textAreaLineStartCached() instead. // Fallback: linear scan (only reachable if cache isn't built yet) 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; } // O(1) line start via cache static int32_t textAreaLineStartCached(WidgetT *w, int32_t row) { TextAreaDataT *ta = (TextAreaDataT *)w->data; textAreaEnsureCache(w); if (row < 0) { return 0; } if (row >= ta->cachedLines) { return ta->len; } return ta->lineOffsets[row]; } // O(1) line length via cache static int32_t textAreaLineLenCached(WidgetT *w, int32_t row) { TextAreaDataT *ta = (TextAreaDataT *)w->data; textAreaEnsureCache(w); if (row < 0 || row >= ta->cachedLines) { return 0; } int32_t start = ta->lineOffsets[row]; int32_t next = ta->lineOffsets[row + 1]; // Subtract newline if present if (next > start && ta->buf[next - 1] == '\n') { next--; } return next - start; } 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 textAreaGetMaxLineLen(WidgetT *w) { textAreaEnsureCache(w); TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->cachedMaxLL < 0 && ta->cachedLines >= 0) { // Recompute max from cached visual lengths int32_t maxVL = 0; for (int32_t i = 0; i < ta->cachedLines; i++) { if (ta->lineVisLens[i] > maxVL) { maxVL = ta->lineVisLens[i]; } } ta->cachedMaxLL = maxVL; } return ta->cachedMaxLL; } static inline void textAreaDirtyCache(WidgetT *w) { TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->cachedLines = -1; ta->cachedMaxLL = -1; ta->cachedGutterW = 0; } // Incrementally update the cache after inserting bytes at `off`. // If the insertion contains newlines, falls back to a full rebuild. // Otherwise, adjusts offsets and visual lengths in O(lines_after_cursor). static void textAreaCacheInsert(WidgetT *w, int32_t off, int32_t insertLen) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->cachedLines < 0 || insertLen <= 0) { textAreaDirtyCache(w); return; } // Check if inserted bytes contain newlines — if so, full rebuild const char *buf = ta->buf; for (int32_t i = off; i < off + insertLen && i < ta->len; i++) { if (buf[i] == '\n') { textAreaDirtyCache(w); return; } } // Find which line the insertion is on int32_t line = 0; for (line = ta->cachedLines - 1; line > 0; line--) { if (ta->lineOffsets[line] <= off) { break; } } // Shift all subsequent line offsets for (int32_t i = line + 1; i <= ta->cachedLines; i++) { ta->lineOffsets[i] += insertLen; } // Recompute visual length of the affected line int32_t lineOff = ta->lineOffsets[line]; int32_t lineEnd = (line + 1 <= ta->cachedLines) ? ta->lineOffsets[line + 1] : ta->len; int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 4; 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++; } } ta->lineVisLens[line] = vc; // Update max line length if (vc > ta->cachedMaxLL) { ta->cachedMaxLL = vc; } else { ta->cachedMaxLL = -1; // unknown, will recompute lazily } ta->cachedGutterW = 0; } // Incrementally update the cache after deleting bytes at `off`. // If the deletion spans newlines, falls back to a full rebuild. static void textAreaCacheDelete(WidgetT *w, int32_t off, int32_t deleteLen) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->cachedLines < 0 || deleteLen <= 0) { textAreaDirtyCache(w); return; } // Check if deleted bytes contained newlines — check the buffer BEFORE deletion // Since deletion already happened, we can't check. Fall back to dirty for safety // unless we know it was a single non-newline character. if (deleteLen > 1) { textAreaDirtyCache(w); return; } // Single character delete — check if a line was removed by seeing if // line count decreased. Quick check: the character that WAS at `off` // is now gone. If the current char at `off` merged two lines, rebuild. // Simple heuristic: if the line count from offsets doesn't match after // adjusting, rebuild. // Find which line the deletion was on int32_t line = 0; for (line = ta->cachedLines - 1; line > 0; line--) { if (ta->lineOffsets[line] <= off) { break; } } // Shift subsequent offsets for (int32_t i = line + 1; i <= ta->cachedLines; i++) { ta->lineOffsets[i] -= deleteLen; } // Verify the line structure is still valid int32_t lineOff = ta->lineOffsets[line]; int32_t nextOff = (line + 1 <= ta->cachedLines) ? ta->lineOffsets[line + 1] : ta->len; // If the next line's offset is now <= this line's, a newline was deleted if (line + 1 <= ta->cachedLines && nextOff <= lineOff) { textAreaDirtyCache(w); return; } // Check that no newline appears before the expected boundary const char *buf = ta->buf; for (int32_t i = lineOff; i < nextOff && i < ta->len; i++) { if (buf[i] == '\n') { if (i < nextOff - 1) { // Newline in the middle — structure changed textAreaDirtyCache(w); return; } } } // Recompute visual length of the affected line int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 4; int32_t vc = 0; for (int32_t i = lineOff; i < nextOff && i < ta->len && buf[i] != '\n'; i++) { if (buf[i] == '\t') { vc += tabW - (vc % tabW); } else { vc++; } } ta->lineVisLens[line] = vc; ta->cachedMaxLL = -1; // recompute lazily ta->cachedGutterW = 0; } // Tab-aware visual column from buffer offset within a line. // tabW <= 0 means tabs are 1 column (no expansion). static int32_t visualCol(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. static int32_t visualColToOff(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 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; } // O(log N) offset-to-row/col using binary search on cached line offsets. // Falls back to linear scan if cache is not available. static void textAreaOffToRowColFast(TextAreaDataT *ta, int32_t off, int32_t *row, int32_t *col) { if (!ta->lineOffsets || ta->cachedLines < 0) { textAreaOffToRowCol(ta->buf, off, row, col); return; } // Binary search for the row containing 'off' int32_t lo = 0; int32_t hi = ta->cachedLines; while (lo < hi) { int32_t mid = (lo + hi + 1) / 2; if (ta->lineOffsets[mid] <= off) { lo = mid; } else { hi = mid - 1; } } *row = lo; *col = off - ta->lineOffsets[lo]; } static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols) { TextAreaDataT *ta = (TextAreaDataT *)w->data; int32_t row = ta->cursorRow; // Use cached cursor offset to avoid O(N) recomputation int32_t curLineOff = textAreaLineStartCached(w, row); int32_t curOff = curLineOff + ta->cursorCol; if (curOff > ta->len) { curOff = ta->len; } int32_t col = visualCol(ta->buf, curLineOff, curOff, ta->tabWidth); if (row < ta->scrollRow) { ta->scrollRow = row; } if (row >= ta->scrollRow + visRows) { ta->scrollRow = row - visRows + 1; } if (col < ta->scrollCol) { ta->scrollCol = col; } if (col >= ta->scrollCol + visCols) { ta->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) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta) { free(ta->buf); free(ta->undoBuf); free(ta->lineOffsets); free(ta->lineVisLens); free(ta->rawSyntax); free(ta->expandBuf); free(ta->syntaxBuf); free(ta); w->data = NULL; } } // ============================================================ // widgetTextAreaGetText // ============================================================ const char *widgetTextAreaGetText(const WidgetT *w) { const TextAreaDataT *ta = (const TextAreaDataT *)w->data; return ta->buf ? ta->buf : ""; } // ============================================================ // widgetTextAreaOnKey // ============================================================ // TextArea key handling is inline (not using widgetTextEditOnKey) // because multi-line editing has fundamentally different cursor // semantics: row/col instead of linear offset, desiredCol for // vertical movement (so moving down from a long line to a short // line remembers the original column), and Enter inserts newlines. // // The SEL_BEGIN/SEL_END/HAS_SEL macros factor out the repetitive // selection-start/selection-extend pattern: SEL_BEGIN initializes // the anchor at the current offset if Shift is held and no selection // exists yet. SEL_END updates the selection cursor to the new // position (or clears selection if Shift isn't held). This keeps // the per-key handler code manageable despite the large number of // key combinations. // // textAreaEnsureVisible() is called after every cursor movement to // auto-scroll the viewport. It adjusts scrollRow/scrollCol so the // cursor is within the visible range. void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (!ta->buf) { return; } clearOtherSelections(w); char *buf = ta->buf; int32_t bufSize = ta->bufSize; int32_t *pLen = &ta->len; int32_t *pRow = &ta->cursorRow; int32_t *pCol = &ta->cursorCol; int32_t *pSA = &ta->selAnchor; int32_t *pSC = &ta->selCursor; bool shift = (mod & KEY_MOD_SHIFT) != 0; AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t gutterW = textAreaGutterWidth(w, font); int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaGetMaxLineLen(w); 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 = textAreaGetLineCount(w); // 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) // Clamp selection to buffer bounds if (HAS_SEL()) { if (*pSA > *pLen) { *pSA = *pLen; } if (*pSC > *pLen) { *pSC = *pLen; } } // Ctrl+A -- select all if (key == 1) { *pSA = 0; *pSC = *pLen; textAreaOffToRowColFast(ta, *pLen, pRow, pCol); ta->desiredCol = *pCol; textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Ctrl+C -- copy if (key == 3) { if (HAS_SEL()) { clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); } return; } // Read-only: allow select-all, copy, and navigation but block editing if (w->readOnly) { goto navigation; } // Ctrl+V -- paste if (key == 22) { int32_t clipLen = 0; const char *clip = clipboardGet(&clipLen); if (clipLen > 0) { textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->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); textAreaOffToRowColFast(ta, lo, pRow, pCol); *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; textAreaOffToRowColFast(ta, off + paste, pRow, pCol); ta->desiredCol = *pCol; } if (w->onChange) { w->onChange(w); } textAreaDirtyCache(w); } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Ctrl+X -- cut if (key == 24) { if (HAS_SEL()) { clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); textAreaOffToRowColFast(ta, lo, pRow, pCol); *pSA = -1; *pSC = -1; ta->desiredCol = *pCol; if (w->onChange) { w->onChange(w); } textAreaDirtyCache(w); } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Ctrl+Z -- undo if (key == 26) { if (ta->undoBuf && ta->undoLen >= 0) { // Swap current and undo 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 = ta->undoLen < bufSize - 1 ? ta->undoLen : bufSize - 1; memcpy(buf, ta->undoBuf, restLen); buf[restLen] = '\0'; *pLen = restLen; // Save current as new undo int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; memcpy(ta->undoBuf, tmpBuf, saveLen); ta->undoBuf[saveLen] = '\0'; ta->undoLen = saveLen; ta->undoCursor = tmpCursor; // Restore cursor int32_t restoreOff = ta->undoCursor < *pLen ? ta->undoCursor : *pLen; ta->undoCursor = tmpCursor; textAreaOffToRowColFast(ta, restoreOff, pRow, pCol); ta->desiredCol = *pCol; *pSA = -1; *pSC = -1; if (w->onChange) { w->onChange(w); } textAreaDirtyCache(w); } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Enter -- insert newline if (key == 0x0D) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->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); textAreaOffToRowColFast(ta, lo, pRow, pCol); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); // Measure indent of current line before inserting newline int32_t indent = 0; char indentBuf[64]; if (ta->autoIndent) { int32_t lineStart = textAreaLineStart(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; ta->desiredCol = indent; } if (w->onChange) { w->onChange(w); } textAreaDirtyCache(w); } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Backspace if (key == 8) { if (HAS_SEL()) { textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); textAreaOffToRowColFast(ta, lo, pRow, pCol); *pSA = -1; *pSC = -1; ta->desiredCol = *pCol; textAreaDirtyCache(w); if (w->onChange) { w->onChange(w); } } else { int32_t off = CUR_OFF(); if (off > 0) { char deleted = buf[off - 1]; textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); memmove(buf + off - 1, buf + off, *pLen - off + 1); (*pLen)--; textAreaOffToRowColFast(ta, off - 1, pRow, pCol); ta->desiredCol = *pCol; if (deleted == '\n') { textAreaDirtyCache(w); } else { textAreaCacheDelete(w, off - 1, 1); } if (w->onChange) { w->onChange(w); } } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Delete if (key == (0x53 | 0x100)) { if (HAS_SEL()) { textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); int32_t lo = SEL_LO(); int32_t hi = SEL_HI(); memmove(buf + lo, buf + hi, *pLen - hi + 1); *pLen -= (hi - lo); textAreaOffToRowColFast(ta, lo, pRow, pCol); *pSA = -1; *pSC = -1; ta->desiredCol = *pCol; textAreaDirtyCache(w); if (w->onChange) { w->onChange(w); } } else { int32_t off = CUR_OFF(); if (off < *pLen) { char deleted = buf[off]; textEditSaveUndo(buf, *pLen, off, ta->undoBuf, &ta->undoLen, &ta->undoCursor, bufSize); memmove(buf + off, buf + off + 1, *pLen - off); (*pLen)--; if (deleted == '\n') { textAreaDirtyCache(w); } else { textAreaCacheDelete(w, off, 1); } if (w->onChange) { w->onChange(w); } } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } navigation: // Left arrow if (key == (0x4B | 0x100)) { SEL_BEGIN(); int32_t off = CUR_OFF(); if (off > 0) { textAreaOffToRowColFast(ta, off - 1, pRow, pCol); } ta->desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Right arrow if (key == (0x4D | 0x100)) { SEL_BEGIN(); int32_t off = CUR_OFF(); if (off < *pLen) { textAreaOffToRowColFast(ta, off + 1, pRow, pCol); } ta->desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Ctrl+Left -- word left if (key == (0x73 | 0x100)) { SEL_BEGIN(); int32_t off = CUR_OFF(); int32_t newOff = wordBoundaryLeft(buf, off); textAreaOffToRowColFast(ta, newOff, pRow, pCol); ta->desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); 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); textAreaOffToRowColFast(ta, newOff, pRow, pCol); ta->desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Up arrow if (key == (0x48 | 0x100)) { SEL_BEGIN(); if (*pRow > 0) { (*pRow)--; int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); *pCol = ta->desiredCol < lineL ? ta->desiredCol : lineL; } SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Down arrow if (key == (0x50 | 0x100)) { SEL_BEGIN(); if (*pRow < totalLines - 1) { (*pRow)++; int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); *pCol = ta->desiredCol < lineL ? ta->desiredCol : lineL; } SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Home if (key == (0x47 | 0x100)) { SEL_BEGIN(); *pCol = 0; ta->desiredCol = 0; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // End if (key == (0x4F | 0x100)) { SEL_BEGIN(); *pCol = textAreaLineLen(buf, *pLen, *pRow); ta->desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(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 = ta->desiredCol < lineL ? ta->desiredCol : lineL; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(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 = ta->desiredCol < lineL ? ta->desiredCol : lineL; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Ctrl+Home (scancode 0x77) if (key == (0x77 | 0x100)) { SEL_BEGIN(); *pRow = 0; *pCol = 0; ta->desiredCol = 0; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Ctrl+End (scancode 0x75) if (key == (0x75 | 0x100)) { SEL_BEGIN(); textAreaOffToRowColFast(ta, *pLen, pRow, pCol); ta->desiredCol = *pCol; SEL_END(); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } // Tab key -- insert tab character or spaces if captureTabs is enabled if (key == 9 && ta->captureTabs && !w->readOnly) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->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); textAreaOffToRowColFast(ta, lo, pRow, pCol); *pSA = -1; *pSC = -1; } int32_t off = CUR_OFF(); if (ta->useTabChar) { // Insert a single tab character if (*pLen < bufSize - 1) { memmove(buf + off + 1, buf + off, *pLen - off + 1); buf[off] = '\t'; (*pLen)++; (*pCol)++; ta->desiredCol = *pCol; } } else { // Insert spaces to next tab stop int32_t tw = ta->tabWidth > 0 ? ta->tabWidth : 3; int32_t spaces = tw - (*pCol % tw); 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)++; } ta->desiredCol = *pCol; } textAreaDirtyCache(w); textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); } return; } // Printable character (blocked in read-only mode) if (key >= 32 && key < 127 && !w->readOnly) { if (*pLen < bufSize - 1) { textEditSaveUndo(buf, *pLen, CUR_OFF(), ta->undoBuf, &ta->undoLen, &ta->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); textAreaOffToRowColFast(ta, 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)++; ta->desiredCol = *pCol; textAreaCacheInsert(w, off, 1); } else { textAreaDirtyCache(w); } if (w->onChange) { w->onChange(w); } } textAreaEnsureVisible(w, visRows, visCols); wgtInvalidatePaint(w); return; } #undef CUR_OFF #undef SEL_BEGIN #undef SEL_END #undef HAS_SEL #undef SEL_LO #undef SEL_HI } // ============================================================ // widgetTextAreaOnMouse // ============================================================ // Mouse handling: scrollbar clicks (both V and H), then content area // clicks. Content clicks convert pixel coordinates to row/col using // font metrics and scroll offset. Multi-click: double-click selects // word, triple-click selects entire line. Single click starts a // drag-select (sets sDragWidget which the event loop monitors // on mouse-move to extend the selection). The drag-select global // is cleared on double/triple click since the selection is already // complete. void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { TextAreaDataT *ta = (TextAreaDataT *)w->data; sFocusedWidget = w; clearOtherSelections(w); AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t gutterW = textAreaGutterWidth(w, font); int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW; int32_t innerY = w->y + TEXTAREA_BORDER; int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaGetMaxLineLen(w); 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 = textAreaGetLineCount(w); int32_t maxScroll = totalLines - visRows; if (maxScroll < 0) { maxScroll = 0; } ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll); int32_t maxHScroll = maxLL - visCols; if (maxHScroll < 0) { maxHScroll = 0; } ta->scrollCol = clampInt(ta->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 (ta->scrollCol > 0) { ta->scrollCol--; } } else if (relX >= hsbW - TEXTAREA_SB_W) { // Right arrow if (ta->scrollCol < maxHScroll) { ta->scrollCol++; } } else if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, maxLL, visCols, ta->scrollCol, &thumbPos, &thumbSize); int32_t trackRelX = relX - TEXTAREA_SB_W; if (trackRelX < thumbPos) { ta->scrollCol -= visCols; ta->scrollCol = clampInt(ta->scrollCol, 0, maxHScroll); } else if (trackRelX >= thumbPos + thumbSize) { ta->scrollCol += visCols; ta->scrollCol = clampInt(ta->scrollCol, 0, maxHScroll); } else { sDragWidget = w; ta->sbDragOrient = 1; ta->sbDragging = true; ta->sbDragOff = trackRelX - thumbPos; return; } } 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 (ta->scrollRow > 0) { ta->scrollRow--; } } else if (relY >= sbH - TEXTAREA_SB_W) { // Down arrow if (ta->scrollRow < maxScroll) { ta->scrollRow++; } } else if (trackLen > 0) { int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalLines, visRows, ta->scrollRow, &thumbPos, &thumbSize); int32_t trackRelY = relY - TEXTAREA_SB_W; if (trackRelY < thumbPos) { ta->scrollRow -= visRows; ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll); } else if (trackRelY >= thumbPos + thumbSize) { ta->scrollRow += visRows; ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll); } else { sDragWidget = w; ta->sbDragOrient = 0; ta->sbDragging = true; ta->sbDragOff = trackRelY - thumbPos; return; } } return; } // Click on gutter — toggle breakpoint if (gutterW > 0 && vx < innerX && ta->onGutterClick) { int32_t relY = vy - innerY; int32_t clickRow = ta->scrollRow + relY / font->charHeight; if (clickRow >= 0 && clickRow < totalLines) { ta->onGutterClick(w, clickRow + 1); // 1-based line number } wgtInvalidatePaint(w); return; } // Click on text area -- place cursor int32_t relX = vx - innerX; int32_t relY = vy - innerY; int32_t clickRow = ta->scrollRow + relY / font->charHeight; int32_t clickVisCol = ta->scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= totalLines) { clickRow = totalLines - 1; } if (clickVisCol < 0) { clickVisCol = 0; } int32_t clkLineStart = textAreaLineStartCached(w, clickRow); int32_t clkByteOff = visualColToOff(ta->buf, ta->len, clkLineStart, clickVisCol, ta->tabWidth); int32_t clickCol = clkByteOff - clkLineStart; int32_t lineL = textAreaLineLenCached(w, clickRow); if (clickCol > lineL) { clickCol = lineL; } int32_t clicks = multiClickDetect(vx, vy); if (clicks >= 3) { // Triple-click: select entire line int32_t lineStart = textAreaCursorToOff(ta->buf, ta->len, clickRow, 0); int32_t lineEnd = textAreaCursorToOff(ta->buf, ta->len, clickRow, lineL); ta->cursorRow = clickRow; ta->cursorCol = lineL; ta->desiredCol = lineL; ta->selAnchor = lineStart; ta->selCursor = lineEnd; sDragWidget = NULL; return; } if (clicks == 2 && ta->buf) { // Double-click: select word int32_t off = textAreaCursorToOff(ta->buf, ta->len, clickRow, clickCol); int32_t ws = wordStart(ta->buf, off); int32_t we = wordEnd(ta->buf, ta->len, off); int32_t weRow; int32_t weCol; textAreaOffToRowColFast(ta, we, &weRow, &weCol); ta->cursorRow = weRow; ta->cursorCol = weCol; ta->desiredCol = weCol; ta->selAnchor = ws; ta->selCursor = we; sDragWidget = NULL; return; } // Single click: place cursor + start drag-select ta->cursorRow = clickRow; ta->cursorCol = clickCol; ta->desiredCol = clickCol; int32_t anchorOff = textAreaCursorToOff(ta->buf, ta->len, clickRow, clickCol); ta->selAnchor = anchorOff; ta->selCursor = anchorOff; sDragWidget = w; ta->sbDragging = false; } // ============================================================ // syntaxColor // ============================================================ static uint32_t syntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const ColorSchemeT *colors) { (void)colors; switch (idx) { case SYNTAX_KEYWORD: return packColor(d, 0, 0, 128); // dark blue case SYNTAX_STRING: return packColor(d, 128, 0, 0); // dark red case SYNTAX_COMMENT: return packColor(d, 0, 128, 0); // dark green case SYNTAX_NUMBER: return packColor(d, 128, 0, 128); // purple case SYNTAX_OPERATOR: return packColor(d, 128, 128, 0); // dark yellow case SYNTAX_TYPE: return packColor(d, 0, 128, 128); // teal default: return defaultFg; } } // ============================================================ // drawColorizedText // ============================================================ // // Draw text with per-character syntax coloring. Batches consecutive // characters of the same color into single drawTextN calls. static void drawColorizedText(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 ColorSchemeT *colors) { 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 = syntaxColor(d, curColor, defaultFg, colors); drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true); runStart = runEnd; } } // ============================================================ // widgetTextAreaPaint // ============================================================ // TextArea paint uses an optimized incremental line-offset approach: // instead of calling textAreaLineStart() for each visible row (which // would re-scan from the buffer start each time), we compute the // starting offset of the first visible line once, then advance it // line by line. This makes the paint cost O(visRows * maxLineLen) // rather than O(visRows * totalLines). // // Each line is drawn in up to 3 runs (before-selection, selection, // after-selection) to avoid overdraw. Selection highlighting that // extends past the end of a line (the newline itself is "selected") // is rendered as a highlight-colored rectFill past the text. // // Scrollbars are drawn inline (not using the shared scrollbar // functions) because TextArea scrolling is in rows/cols (logical // units) rather than pixels, so the thumb calculation parameters // differ. The dead corner between scrollbars is filled with // windowFace color. void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { TextAreaDataT *ta = (TextAreaDataT *)w->data; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; char *buf = ta->buf; int32_t len = ta->len; int32_t gutterW = textAreaGutterWidth(w, font); int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t maxLL = textAreaGetMaxLineLen(w); 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 = textAreaGetLineCount(w); 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; } ta->scrollRow = clampInt(ta->scrollRow, 0, maxScroll); // Clamp horizontal scroll int32_t maxHScroll = maxLL - visCols; if (maxHScroll < 0) { maxHScroll = 0; } ta->scrollCol = clampInt(ta->scrollCol, 0, maxHScroll); // Selection range int32_t selLo = -1; int32_t selHi = -1; if (ta->selAnchor >= 0 && ta->selCursor >= 0 && ta->selAnchor != ta->selCursor) { selLo = ta->selAnchor < ta->selCursor ? ta->selAnchor : ta->selCursor; selHi = ta->selAnchor < ta->selCursor ? ta->selCursor : ta->selAnchor; } // Draw lines -- compute first visible line offset once, then advance incrementally int32_t gutterX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t textX = gutterX + gutterW; int32_t textY = w->y + TEXTAREA_BORDER; int32_t lineOff = textAreaLineStartCached(w, ta->scrollRow); // Draw gutter background if (gutterW > 0) { rectFill(d, ops, gutterX, textY, gutterW, innerH, colors->windowFace); drawVLine(d, ops, gutterX + gutterW - 1, textY, innerH, colors->windowShadow); } for (int32_t i = 0; i < visRows; i++) { int32_t row = ta->scrollRow + i; if (row >= totalLines) { break; } // Use cached line length (O(1) lookup instead of O(lineLen) scan) int32_t lineL = textAreaLineLenCached(w, row); int32_t drawY = textY + i * font->charHeight; // Line decorator: background highlight and gutter indicators uint32_t lineBg = bg; uint32_t gutterColor = 0; if (ta->lineDecorator) { uint32_t decBg = ta->lineDecorator(row + 1, &gutterColor, ta->lineDecoratorCtx); if (decBg) { lineBg = decBg; } // Always fill the line background when a decorator is active // to clear stale highlights from previous paints. rectFill(d, ops, textX, drawY, innerW, font->charHeight, lineBg); } // Draw line number in gutter if (gutterW > 0) { // Gutter indicator (breakpoint dot) if (gutterColor) { // Draw a filled circle using rectFill scanlines. // Radius 3 gives a clean 7x7 circle at any font size. int32_t cx = gutterX + font->charWidth / 2 + 1; int32_t cy = drawY + font->charHeight / 2; // dy: -3 -2 -1 0 1 2 3 static const int32_t hw[] = { 1, 2, 3, 3, 3, 2, 1 }; for (int32_t dy = -3; dy <= 3; dy++) { int32_t w = hw[dy + 3]; rectFill(d, ops, cx - w, cy + dy, w * 2 + 1, 1, gutterColor); } } char numBuf[12]; int32_t numLen = snprintf(numBuf, sizeof(numBuf), "%d", (int)(row + 1)); int32_t numX = gutterX + gutterW - (numLen + 1) * font->charWidth; drawTextN(d, ops, font, numX, drawY, numBuf, numLen, colors->windowShadow, colors->windowFace, true); } // Override bg for this line if the decorator set a custom background. // This ensures text character backgrounds match the highlighted line. uint32_t savedBg = bg; bg = lineBg; // Expand tabs in this line into a temporary buffer for drawing. // Also expand syntax colors so each visual column has a color byte. int32_t tabW = ta->tabWidth > 0 ? ta->tabWidth : 3; // Compute syntax colors for the raw line first (before expansion) uint8_t *rawSyntax = ta->rawSyntax; char *expandBuf = ta->expandBuf; uint8_t *syntaxBuf = ta->syntaxBuf; bool hasSyntax = false; if (ta->colorize && lineL > 0) { int32_t colorLen = lineL < MAX_COLORIZE_LEN ? lineL : MAX_COLORIZE_LEN; memset(rawSyntax, 0, colorLen); ta->colorize(buf + lineOff, colorLen, rawSyntax, ta->colorizeCtx); hasSyntax = true; } int32_t expandLen = 0; int32_t vc = 0; for (int32_t j = 0; j < lineL && expandLen < MAX_COLORIZE_LEN - tabW; j++) { if (buf[lineOff + j] == '\t') { int32_t spaces = tabW - (vc % tabW); uint8_t sc = hasSyntax && j < MAX_COLORIZE_LEN ? rawSyntax[j] : 0; for (int32_t s = 0; s < spaces && expandLen < MAX_COLORIZE_LEN; s++) { expandBuf[expandLen] = ' '; syntaxBuf[expandLen] = sc; expandLen++; vc++; } } else { expandBuf[expandLen] = buf[lineOff + j]; syntaxBuf[expandLen] = hasSyntax && j < MAX_COLORIZE_LEN ? rawSyntax[j] : 0; expandLen++; vc++; } } // Visible range within expanded line (visual columns) int32_t scrollCol = ta->scrollCol; int32_t visStart = scrollCol; int32_t visEnd = scrollCol + visCols; int32_t textEnd = expandLen; // Clamp visible range to actual expanded content int32_t drawStart = visStart < textEnd ? visStart : textEnd; int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd; // Determine selection intersection with this line. // Selection offsets are byte-based; convert to visual columns in the expanded buffer. 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 = visualCol(buf, lineOff, lineOff + byteSelLo, tabW); lineSelHi = visualCol(buf, lineOff, lineOff + (byteSelHi < lineL ? byteSelHi : lineL), tabW); if (byteSelHi > lineL) { lineSelHi = expandLen + 1; // extends past EOL } } } if (lineSelLo >= 0 && lineSelLo < lineSelHi) { int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo; int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd; if (vSelLo > vSelHi) { vSelLo = vSelHi; } // Before selection if (drawStart < vSelLo) { if (hasSyntax) { drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, colors); } else { drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, fg, bg, true); } } // Selection (always uses highlight colors, no syntax coloring) if (vSelLo < vSelHi) { drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, expandBuf + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); } // After selection if (vSelHi < drawEnd) { if (hasSyntax) { drawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, colors); } else { drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true); } } // Past end of text: fill selected area with highlight bg 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 { // No selection on this line if (drawStart < drawEnd) { if (hasSyntax) { drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, colors); } else { drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true); } } } // Restore bg for next line bg = savedBg; // Advance lineOff to the next line lineOff += lineL; if (lineOff < len && buf[lineOff] == '\n') { lineOff++; } } // Draw cursor (blinks at same rate as terminal cursor) if (w == sFocusedWidget && sCursorBlinkOn) { int32_t curLineOff = textAreaLineStartCached(w, ta->cursorRow); int32_t curOff = curLineOff + ta->cursorCol; if (curOff > len) { curOff = len; } int32_t curVisCol = visualCol(buf, curLineOff, curOff, ta->tabWidth); int32_t curDrawCol = curVisCol - ta->scrollCol; int32_t curDrawRow = ta->cursorRow - ta->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, ta->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, ta->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 == sFocusedWidget) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); } } // ============================================================ // widgetTextAreaSetText // ============================================================ void widgetTextAreaSetText(WidgetT *w, const char *text) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->buf) { strncpy(ta->buf, text, ta->bufSize - 1); ta->buf[ta->bufSize - 1] = '\0'; ta->len = (int32_t)strlen(ta->buf); ta->cursorRow = 0; ta->cursorCol = 0; ta->scrollRow = 0; ta->scrollCol = 0; ta->desiredCol = 0; ta->selAnchor = -1; ta->selCursor = -1; ta->cachedLines = -1; ta->cachedMaxLL = -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) { TextInputDataT *ti = (TextInputDataT *)w->data; if (ti) { free(ti->buf); free(ti->undoBuf); free(ti); w->data = NULL; } } // ============================================================ // widgetTextInputGetText // ============================================================ const char *widgetTextInputGetText(const WidgetT *w) { const TextInputDataT *ti = (const TextInputDataT *)w->data; return ti->buf ? ti->buf : ""; } // ============================================================ // widgetTextInputOnKey // ============================================================ // TextInput key handling delegates to the shared widgetTextEditOnKey // engine, passing pointers to its state fields. Masked input mode // gets its own handler since the editing semantics are completely // different. Password mode blocks copy/cut at this level before // reaching the shared engine. void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) { TextInputDataT *ti = (TextInputDataT *)w->data; if (!ti->buf) { return; } clearOtherSelections(w); if (ti->inputMode == InputMaskedE) { maskedInputOnKey(w, key, mod); return; } // Password mode: block copy (Ctrl+C) and cut (Ctrl+X) if (ti->inputMode == InputPasswordE) { if (key == 3 || key == 24) { return; } } widgetTextEditOnKey(w, key, mod, ti->buf, ti->bufSize, &ti->len, &ti->cursorPos, &ti->scrollOff, &ti->selStart, &ti->selEnd, ti->undoBuf, &ti->undoLen, &ti->undoCursor, w->w - TEXT_INPUT_PAD * 2); } // ============================================================ // widgetTextInputOnMouse // ============================================================ // Mouse handling for single-line input. Cursor position is computed // from pixel offset using the fixed-width font (relX / charWidth + // scrollOff). Multi-click: double-click selects word (using // wordStart/wordEnd), triple-click selects all. Single click starts // drag-select by setting both selStart and selEnd to the click // position and registering sDragWidget. void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { TextInputDataT *ti = (TextInputDataT *)w->data; sFocusedWidget = w; clearOtherSelections(w); AppContextT *ctx = (AppContextT *)root->userData; widgetTextEditMouseClick(w, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, ti->buf, ti->len, ti->scrollOff, &ti->cursorPos, &ti->selStart, &ti->selEnd, true, true); } // ============================================================ // widgetTextInputPaint // ============================================================ // TextInput paint: sunken 2px bevel, then text with optional selection // highlighting, then cursor line. Text is drawn from a display buffer // that may be either the actual text or bullets (password mode, using // CP437 character 0xF9 which renders as a small centered dot). // // The 3-run approach (before/during/after selection) draws text in a // single pass without overdraw. The scroll offset ensures only the // visible portion of the text is drawn. The cursor is a 1px-wide // vertical line drawn at the character boundary. void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { TextInputDataT *ti = (TextInputDataT *)w->data; uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow; 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 (ti->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 = ti->scrollOff; int32_t len = ti->len - off; if (len > maxChars) { len = maxChars; } bool isPassword = (ti->inputMode == InputPasswordE); // Build display buffer (password masking) char dispBuf[256]; int32_t dispLen = len > 255 ? 255 : len; if (isPassword) { memset(dispBuf, '\xF9', dispLen); // CP437 bullet } else { memcpy(dispBuf, ti->buf + off, dispLen); } widgetTextEditPaintLine(d, ops, font, colors, textX, textY, dispBuf, dispLen, off, ti->cursorPos, ti->selStart, ti->selEnd, fg, bg, w == sFocusedWidget && w->enabled, w->x + TEXT_INPUT_PAD, w->x + w->w - TEXT_INPUT_PAD); } } // ============================================================ // widgetTextInputSetText // ============================================================ void widgetTextInputSetText(WidgetT *w, const char *text) { TextInputDataT *ti = (TextInputDataT *)w->data; if (ti->buf) { strncpy(ti->buf, text, ti->bufSize - 1); ti->buf[ti->bufSize - 1] = '\0'; ti->len = (int32_t)strlen(ti->buf); ti->cursorPos = ti->len; ti->scrollOff = 0; ti->selStart = -1; ti->selEnd = -1; } } // ============================================================ // DXE registration // ============================================================ static bool widgetTextInputClearSelection(WidgetT *w) { TextInputDataT *ti = (TextInputDataT *)w->data; if (ti->selStart >= 0 && ti->selEnd >= 0 && ti->selStart != ti->selEnd) { ti->selStart = -1; ti->selEnd = -1; return true; } return false; } static void widgetTextInputOnDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vy; TextInputDataT *ti = (TextInputDataT *)w->data; AppContextT *ctx = wgtGetContext(w); int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / ctx->font.charWidth; widgetTextEditDragUpdateLine(vx, w->x + TEXT_INPUT_PAD, maxChars, &ctx->font, ti->len, &ti->cursorPos, &ti->scrollOff, &ti->selEnd); } static const WidgetClassT sClassTextInput = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE, .handlers = { [WGT_METHOD_PAINT] = (void *)widgetTextInputPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetTextInputCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)widgetTextInputOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetTextInputOnKey, [WGT_METHOD_DESTROY] = (void *)widgetTextInputDestroy, [WGT_METHOD_GET_TEXT] = (void *)widgetTextInputGetText, [WGT_METHOD_SET_TEXT] = (void *)widgetTextInputSetText, [WGT_METHOD_CLEAR_SELECTION] = (void *)widgetTextInputClearSelection, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetTextInputOnDragUpdate, } }; // ============================================================ // widgetTextAreaScrollDragUpdate // ============================================================ // Handle scrollbar thumb drag for TextArea vertical and horizontal scrollbars. // The TextArea always reserves space for the V scrollbar on the right. // The H scrollbar appears at the bottom only when the longest line exceeds // visible columns. static void widgetTextAreaScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { TextAreaDataT *ta = (TextAreaDataT *)w->data; AppContextT *ctx = wgtGetContext(w); 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 = textAreaGetMaxLineLen(w); 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; } if (orient == 0) { // Vertical scrollbar drag int32_t totalLines = textAreaGetLineCount(w); int32_t maxScroll = totalLines - visRows; if (maxScroll <= 0) { return; } int32_t trackLen = innerH - TEXTAREA_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, totalLines, visRows, ta->scrollRow, &thumbPos, &thumbSize); int32_t sbY = w->y + TEXTAREA_BORDER; int32_t relMouse = mouseY - sbY - TEXTAREA_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * relMouse) / (trackLen - thumbSize) : 0; ta->scrollRow = clampInt(newScroll, 0, maxScroll); } else if (orient == 1) { // Horizontal scrollbar drag int32_t maxHScroll = maxLL - visCols; if (maxHScroll <= 0) { return; } int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W; int32_t trackLen = hsbW - TEXTAREA_SB_W * 2; int32_t thumbPos; int32_t thumbSize; widgetScrollbarThumb(trackLen, maxLL, visCols, ta->scrollCol, &thumbPos, &thumbSize); int32_t sbX = w->x + TEXTAREA_BORDER; int32_t relMouse = mouseX - sbX - TEXTAREA_SB_W - dragOff; int32_t newScroll = (trackLen > thumbSize) ? (maxHScroll * relMouse) / (trackLen - thumbSize) : 0; ta->scrollCol = clampInt(newScroll, 0, maxHScroll); } } static void widgetTextAreaOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->sbDragging) { widgetTextAreaScrollDragUpdate(w, ta->sbDragOrient, ta->sbDragOff, x, y); } else { widgetTextAreaDragSelect(w, root, x, y); } } static bool widgetTextAreaClearSelection(WidgetT *w) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->selAnchor >= 0 && ta->selCursor >= 0 && ta->selAnchor != ta->selCursor) { ta->selAnchor = -1; ta->selCursor = -1; return true; } return false; } static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; TextAreaDataT *ta = (TextAreaDataT *)w->data; AppContextT *ctx = wgtGetContext(w); const BitmapFontT *font = &ctx->font; int32_t gutterW = textAreaGutterWidth(w, font); int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD + gutterW; int32_t innerY = w->y + TEXTAREA_BORDER; int32_t relX = vx - innerX; int32_t relY = vy - innerY; int32_t totalLines = textAreaGetLineCount(w); int32_t visRows = (w->h - TEXTAREA_BORDER * 2) / font->charHeight; // Auto-scroll when dragging past edges if (relY < 0 && ta->scrollRow > 0) { ta->scrollRow--; } else if (relY >= visRows * font->charHeight && ta->scrollRow < totalLines - visRows) { ta->scrollRow++; } int32_t clickRow = ta->scrollRow + relY / font->charHeight; int32_t clickVisCol = ta->scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; } if (clickRow >= totalLines) { clickRow = totalLines - 1; } if (clickVisCol < 0) { clickVisCol = 0; } int32_t dragLineStart = textAreaLineStartCached(w, clickRow); int32_t dragByteOff = visualColToOff(ta->buf, ta->len, dragLineStart, clickVisCol, ta->tabWidth); int32_t clickCol = dragByteOff - dragLineStart; int32_t lineL = textAreaLineLenCached(w, clickRow); if (clickCol > lineL) { clickCol = lineL; } ta->cursorRow = clickRow; ta->cursorCol = clickCol; ta->desiredCol = clickCol; ta->selCursor = textAreaCursorToOff(ta->buf, ta->len, clickRow, clickCol); } static const WidgetClassT sClassTextArea = { .version = WGT_CLASS_VERSION, .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE, .handlers = { [WGT_METHOD_PAINT] = (void *)widgetTextAreaPaint, [WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetTextAreaCalcMinSize, [WGT_METHOD_ON_MOUSE] = (void *)widgetTextAreaOnMouse, [WGT_METHOD_ON_KEY] = (void *)widgetTextAreaOnKey, [WGT_METHOD_DESTROY] = (void *)widgetTextAreaDestroy, [WGT_METHOD_GET_TEXT] = (void *)widgetTextAreaGetText, [WGT_METHOD_SET_TEXT] = (void *)widgetTextAreaSetText, [WGT_METHOD_CLEAR_SELECTION] = (void *)widgetTextAreaClearSelection, [WGT_METHOD_ON_DRAG_UPDATE] = (void *)widgetTextAreaOnDragUpdate, } }; // ============================================================ // Widget creation functions // ============================================================ WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask) { if (!mask) { return NULL; } int32_t maskLen = (int32_t)strlen(mask); WidgetT *w = wgtTextInput(parent, maskLen); if (w) { TextInputDataT *ti = (TextInputDataT *)w->data; ti->inputMode = InputMaskedE; ti->mask = mask; // Pre-fill buffer: literals copied as-is, slots filled with '_' for (int32_t i = 0; i < maskLen; i++) { char ch = mask[i]; if (ch == '#' || ch == 'A' || ch == '*') { ti->buf[i] = '_'; } else { ti->buf[i] = ch; } } ti->buf[maskLen] = '\0'; ti->len = maskLen; // Position cursor at first editable slot ti->cursorPos = 0; for (int32_t i = 0; mask[i]; i++) { char ch = mask[i]; if (ch == '#' || ch == 'A' || ch == '*') { ti->cursorPos = i; break; } } } return w; } WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen) { WidgetT *w = wgtTextInput(parent, maxLen); if (w) { TextInputDataT *ti = (TextInputDataT *)w->data; ti->inputMode = InputPasswordE; } return w; } WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) { WidgetT *w = widgetAlloc(parent, sTextAreaTypeId); if (w) { TextAreaDataT *ta = (TextAreaDataT *)calloc(1, sizeof(TextAreaDataT)); if (!ta) { return w; } w->data = ta; int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; ta->buf = (char *)malloc(bufSize); ta->undoBuf = (char *)malloc(bufSize); ta->bufSize = bufSize; if (!ta->buf || !ta->undoBuf) { free(ta->buf); free(ta->undoBuf); ta->buf = NULL; ta->undoBuf = NULL; } else { ta->buf[0] = '\0'; } ta->selAnchor = -1; ta->selCursor = -1; ta->desiredCol = 0; ta->cachedLines = -1; ta->cachedMaxLL = -1; ta->captureTabs = false; // default: Tab moves focus ta->useTabChar = true; // default: insert actual tab character ta->tabWidth = 3; // default: 3-space tab stops // Pre-allocate paint buffers (avoids 3KB stack alloc per visible line per frame) ta->rawSyntax = (uint8_t *)malloc(MAX_COLORIZE_LEN); ta->expandBuf = (char *)malloc(MAX_COLORIZE_LEN); ta->syntaxBuf = (uint8_t *)malloc(MAX_COLORIZE_LEN); w->weight = 100; } return w; } WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { WidgetT *w = widgetAlloc(parent, sTextInputTypeId); if (w) { TextInputDataT *ti = (TextInputDataT *)calloc(1, sizeof(TextInputDataT)); if (!ti) { return w; } w->data = ti; int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; ti->buf = (char *)malloc(bufSize); ti->bufSize = bufSize; if (ti->buf) { ti->buf[0] = '\0'; } ti->undoBuf = (char *)malloc(bufSize); ti->selStart = -1; ti->selEnd = -1; w->weight = 100; } return w; } // ============================================================ // wgtTextAreaSetColorize // ============================================================ void wgtTextAreaGoToLine(WidgetT *w, int32_t line) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; int32_t totalLines = textAreaGetLineCount(w); int32_t row = line - 1; // 1-based to 0-based if (row < 0) { row = 0; } if (row >= totalLines) { row = totalLines - 1; } ta->cursorRow = row; ta->cursorCol = 0; ta->desiredCol = 0; // Select the entire line for visual highlight int32_t lineStart = textAreaLineStartCached(w, row); int32_t lineL = textAreaLineLenCached(w, row); ta->selAnchor = lineStart; ta->selCursor = lineStart + lineL; // Scroll to put the target line near the top of the visible area AppContextT *ctx = wgtGetContext(w); if (ctx) { const BitmapFontT *font = &ctx->font; int32_t gutterW = textAreaGutterWidth(w, font); int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t innerH = w->h - TEXTAREA_BORDER * 2; int32_t visRows = innerH / font->charHeight; if (visCols < 1) { visCols = 1; } if (visRows < 1) { visRows = 1; } // Place target line 1/4 from top for context, then clamp 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; } ta->scrollRow = targetScroll; ta->scrollCol = 0; } wgtInvalidatePaint(w); } void wgtTextAreaSetAutoIndent(WidgetT *w, bool enable) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->autoIndent = enable; } void wgtTextAreaSetColorize(WidgetT *w, void (*fn)(const char *, int32_t, uint8_t *, void *), void *ctx) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->colorize = fn; ta->colorizeCtx = ctx; } void wgtTextAreaSetGutterClickCallback(WidgetT *w, void (*fn)(WidgetT *, int32_t)) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->onGutterClick = fn; } int32_t wgtTextAreaGetCursorLine(const WidgetT *w) { if (!w || w->type != sTextAreaTypeId) { return 1; } const TextAreaDataT *ta = (const TextAreaDataT *)w->data; return ta->cursorRow + 1; // 0-based to 1-based } int32_t wgtTextAreaGetWordAtCursor(const WidgetT *w, char *buf, int32_t bufSize) { buf[0] = '\0'; if (!w || w->type != sTextAreaTypeId || bufSize < 2) { return 0; } const TextAreaDataT *ta = (const TextAreaDataT *)w->data; if (!ta || !ta->buf || ta->len == 0) { return 0; } int32_t off = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol); int32_t ws = wordStart(ta->buf, off); int32_t we = wordEnd(ta->buf, ta->len, off); int32_t len = we - ws; if (len <= 0) { return 0; } if (len >= bufSize) { len = bufSize - 1; } memcpy(buf, ta->buf + ws, len); buf[len] = '\0'; return len; } void wgtTextAreaSetLineDecorator(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->lineDecorator = fn; ta->lineDecoratorCtx = ctx; } void wgtTextAreaSetShowLineNumbers(WidgetT *w, bool show) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->showLineNumbers = show; wgtInvalidatePaint(w); } void wgtTextAreaSetCaptureTabs(WidgetT *w, bool capture) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->captureTabs = capture; w->swallowTab = capture; } void wgtTextAreaSetTabWidth(WidgetT *w, int32_t width) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->tabWidth = width > 0 ? width : 1; } void wgtTextAreaSetUseTabChar(WidgetT *w, bool useChar) { if (!w || w->type != sTextAreaTypeId) { return; } TextAreaDataT *ta = (TextAreaDataT *)w->data; ta->useTabChar = useChar; } bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) { if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) { return false; } TextAreaDataT *ta = (TextAreaDataT *)w->data; int32_t needleLen = strlen(needle); if (needleLen > ta->len) { return false; } // Get current cursor byte position int32_t cursorByte = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol); // Search forward from cursor+1 (or backward from cursor-1). // No wrap-around: returns false if the end/start of the buffer // is reached without finding a match. int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1; int32_t searchLen = ta->len - needleLen + 1; if (searchLen <= 0) { return false; } int32_t count = forward ? (searchLen - startPos) : (startPos + 1); if (count <= 0) { return false; } for (int32_t attempt = 0; attempt < count; attempt++) { int32_t pos; if (forward) { pos = startPos + attempt; } else { pos = startPos - attempt; } if (pos < 0 || pos >= searchLen) { break; } bool match; if (caseSensitive) { match = (memcmp(ta->buf + pos, needle, needleLen) == 0); } else { match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0); } if (match) { // Select the found text ta->selAnchor = pos; ta->selCursor = pos + needleLen; // Move cursor to start of match (forward) or end of match (backward) int32_t cursorOff = forward ? pos : pos + needleLen; textAreaOffToRowColFast(ta, cursorOff, &ta->cursorRow, &ta->cursorCol); ta->desiredCol = ta->cursorCol; // Scroll to show the match AppContextT *ctx = wgtGetContext(w); if (ctx) { const BitmapFontT *font = &ctx->font; int32_t gutterW = textAreaGutterWidth(w, font); int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; int32_t visCols = innerW / font->charWidth; int32_t innerH = w->h - TEXTAREA_BORDER * 2; int32_t visRows = innerH / font->charHeight; if (visCols < 1) { visCols = 1; } if (visRows < 1) { visRows = 1; } textAreaEnsureVisible(w, visRows, visCols); } wgtInvalidatePaint(w); return true; } } return false; } int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive) { if (!w || w->type != sTextAreaTypeId || !needle || !needle[0] || !replacement) { return 0; } TextAreaDataT *ta = (TextAreaDataT *)w->data; int32_t needleLen = strlen(needle); int32_t replLen = strlen(replacement); int32_t delta = replLen - needleLen; int32_t count = 0; if (needleLen > ta->len) { return 0; } // Save undo before any modifications textEditSaveUndo(ta->buf, ta->len, textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol), ta->undoBuf, &ta->undoLen, &ta->undoCursor, ta->bufSize); int32_t pos = 0; while (pos + needleLen <= ta->len) { bool match; if (caseSensitive) { match = (memcmp(ta->buf + pos, needle, needleLen) == 0); } else { match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0); } if (match) { // Check if replacement fits in buffer if (ta->len + delta >= ta->bufSize) { break; } // Shift text after match to make room (or close gap) if (delta != 0) { memmove(ta->buf + pos + replLen, ta->buf + pos + needleLen, ta->len - pos - needleLen); } // Copy replacement in memcpy(ta->buf + pos, replacement, replLen); ta->len += delta; ta->buf[ta->len] = '\0'; count++; pos += replLen; } else { pos++; } } if (count > 0) { // Clear selection ta->selAnchor = -1; ta->selCursor = -1; // Clamp cursor if it's past the end int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol); if (cursorOff > ta->len) { textAreaOffToRowColFast(ta, ta->len, &ta->cursorRow, &ta->cursorCol); } ta->desiredCol = ta->cursorCol; textAreaDirtyCache(w); wgtInvalidatePaint(w); if (w->onChange) { w->onChange(w); } } return count; } // ============================================================ // DXE registration // ============================================================ static const struct { WidgetT *(*create)(WidgetT *parent, int32_t maxLen); WidgetT *(*password)(WidgetT *parent, int32_t maxLen); WidgetT *(*masked)(WidgetT *parent, const char *mask); WidgetT *(*textArea)(WidgetT *parent, int32_t maxLen); void (*setColorize)(WidgetT *w, void (*fn)(const char *, int32_t, uint8_t *, void *), void *ctx); void (*goToLine)(WidgetT *w, int32_t line); void (*setAutoIndent)(WidgetT *w, bool enable); void (*setShowLineNumbers)(WidgetT *w, bool show); void (*setCaptureTabs)(WidgetT *w, bool capture); void (*setTabWidth)(WidgetT *w, int32_t width); void (*setUseTabChar)(WidgetT *w, bool useChar); bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); int32_t (*getCursorLine)(const WidgetT *w); void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); int32_t (*getWordAtCursor)(const WidgetT *w, char *buf, int32_t bufSize); } sApi = { .create = wgtTextInput, .password = wgtPasswordInput, .masked = wgtMaskedInput, .textArea = wgtTextArea, .setColorize = wgtTextAreaSetColorize, .goToLine = wgtTextAreaGoToLine, .setAutoIndent = wgtTextAreaSetAutoIndent, .setShowLineNumbers = wgtTextAreaSetShowLineNumbers, .setCaptureTabs = wgtTextAreaSetCaptureTabs, .setTabWidth = wgtTextAreaSetTabWidth, .setUseTabChar = wgtTextAreaSetUseTabChar, .findNext = wgtTextAreaFindNext, .replaceAll = wgtTextAreaReplaceAll, .setLineDecorator = wgtTextAreaSetLineDecorator, .getCursorLine = wgtTextAreaGetCursorLine, .setGutterClick = wgtTextAreaSetGutterClickCallback, .getWordAtCursor = wgtTextAreaGetWordAtCursor }; // Per-type APIs for the designer static const struct { WidgetT *(*create)(WidgetT *, int32_t); } sTextInputApi = { .create = wgtTextInput }; static const struct { WidgetT *(*create)(WidgetT *, int32_t); } sTextAreaApi = { .create = wgtTextArea }; static const WgtIfaceT sIfaceTextInput = { .basName = "TextBox", .props = NULL, .propCount = 0, .methods = NULL, .methodCount = 0, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, .createArgs = { 256 }, .defaultEvent = "Change" }; static const WgtIfaceT sIfaceTextArea = { .basName = "TextArea", .props = NULL, .propCount = 0, .methods = NULL, .methodCount = 0, .events = NULL, .eventCount = 0, .createSig = WGT_CREATE_PARENT_INT, .createArgs = { 4096 }, .defaultEvent = "Change" }; void wgtRegister(void) { sTextInputTypeId = wgtRegisterClass(&sClassTextInput); sTextAreaTypeId = wgtRegisterClass(&sClassTextArea); // Original combined API (for widgetTextInput.h compatibility) wgtRegisterApi("textinput", &sApi); // Per-type APIs and ifaces (for designer/toolbox) wgtRegisterApi("textbox", &sTextInputApi); wgtRegisterIface("textbox", &sIfaceTextInput); wgtRegisterApi("textarea", &sTextAreaApi); wgtRegisterIface("textarea", &sIfaceTextArea); }