diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index e3b6da3..6988bf5 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -98,6 +98,20 @@ typedef struct { 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; @@ -159,17 +173,21 @@ 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 textAreaCountLines(const char *buf, int32_t len); 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 textAreaMaxLineLen(const char *buf, int32_t len); +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 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 int32_t visualLineLen(const char *buf, int32_t len, int32_t lineStart, 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); @@ -615,31 +633,97 @@ done: // TextArea line helpers // ============================================================ -static int32_t textAreaCountLines(const char *buf, int32_t len) { - int32_t lines = 1; +// ============================================================ +// 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') { - lines++; + lineCount++; } } - return lines; + // 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; } -// Cached line count -- sentinel value -1 means "dirty, recompute". -// This avoids O(N) buffer scans on every paint frame. The cache is -// invalidated (set to -1) by textAreaDirtyCache() after any buffer -// mutation. The same pattern is used for max line length. -static int32_t textAreaGetLineCount(WidgetT *w) { +static void textAreaEnsureCache(WidgetT *w) { TextAreaDataT *ta = (TextAreaDataT *)w->data; if (ta->cachedLines < 0) { - ta->cachedLines = textAreaCountLines(ta->buf, ta->len); + textAreaRebuildCache(w); } +} - return ta->cachedLines; + +static int32_t textAreaGetLineCount(WidgetT *w) { + textAreaEnsureCache(w); + return ((TextAreaDataT *)w->data)->cachedLines; } @@ -668,7 +752,11 @@ static int32_t textAreaGutterWidth(WidgetT *w, const BitmapFontT *font) { 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++) { @@ -685,6 +773,44 @@ static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) { } +// 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; @@ -697,37 +823,9 @@ static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row) { } -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 textAreaGetMaxLineLen(WidgetT *w) { - TextAreaDataT *ta = (TextAreaDataT *)w->data; - - if (ta->cachedMaxLL < 0) { - ta->cachedMaxLL = textAreaMaxLineLen(ta->buf, ta->len); - } - - return ta->cachedMaxLL; + textAreaEnsureCache(w); + return ((TextAreaDataT *)w->data)->cachedMaxLL; } @@ -738,6 +836,66 @@ static inline void textAreaDirtyCache(WidgetT *w) { } +// 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; +} + + +// Tab-aware visual line length (in visual columns). +static int32_t visualLineLen(const char *buf, int32_t len, int32_t lineStart, int32_t tabW) { + int32_t vc = 0; + int32_t i = lineStart; + + while (i < len && buf[i] != '\n') { + if (buf[i] == '\t' && tabW > 0) { + vc += tabW - (vc % tabW); + } else { + vc++; + } + + i++; + } + + return vc; +} + + 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); @@ -768,7 +926,16 @@ static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int3 static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols) { TextAreaDataT *ta = (TextAreaDataT *)w->data; int32_t row = ta->cursorRow; - int32_t col = ta->cursorCol; + + // 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; @@ -808,6 +975,8 @@ void widgetTextAreaDestroy(WidgetT *w) { if (ta) { free(ta->buf); free(ta->undoBuf); + free(ta->lineOffsets); + free(ta->lineVisLens); free(ta); w->data = NULL; } @@ -1606,8 +1775,8 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { int32_t relX = vx - innerX; int32_t relY = vy - innerY; - int32_t clickRow = ta->scrollRow + relY / font->charHeight; - int32_t clickCol = ta->scrollCol + relX / font->charWidth; + int32_t clickRow = ta->scrollRow + relY / font->charHeight; + int32_t clickVisCol = ta->scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; @@ -1617,11 +1786,14 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { clickRow = totalLines - 1; } - if (clickCol < 0) { - clickCol = 0; + if (clickVisCol < 0) { + clickVisCol = 0; } - int32_t lineL = textAreaLineLen(ta->buf, ta->len, clickRow); + 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; @@ -1792,7 +1964,7 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit int32_t gutterX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; int32_t textX = gutterX + gutterW; int32_t textY = w->y + TEXTAREA_BORDER; - int32_t lineOff = textAreaLineStart(buf, len, ta->scrollRow); + int32_t lineOff = textAreaLineStartCached(w, ta->scrollRow); // Draw gutter background if (gutterW > 0) { @@ -1858,36 +2030,74 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit uint32_t savedBg = bg; bg = lineBg; - // Visible range within line - int32_t scrollCol = ta->scrollCol; - int32_t visStart = scrollCol; - int32_t visEnd = scrollCol + visCols; - int32_t textEnd = lineL; // chars in this line + // 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; - // Clamp visible range to actual line content for text drawing - int32_t drawStart = visStart < textEnd ? visStart : textEnd; - int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd; - - // Compute syntax colors for this line if colorizer is set - uint8_t syntaxBuf[MAX_COLORIZE_LEN]; + // Compute syntax colors for the raw line first (before expansion) + uint8_t rawSyntax[MAX_COLORIZE_LEN]; bool hasSyntax = false; if (ta->colorize && lineL > 0) { int32_t colorLen = lineL < MAX_COLORIZE_LEN ? lineL : MAX_COLORIZE_LEN; - memset(syntaxBuf, 0, colorLen); - ta->colorize(buf + lineOff, colorLen, syntaxBuf, ta->colorizeCtx); + memset(rawSyntax, 0, colorLen); + ta->colorize(buf + lineOff, colorLen, rawSyntax, ta->colorizeCtx); hasSyntax = true; } - // Determine selection intersection with this line + // Expand tabs: build visual text and expanded syntax buffers + char expandBuf[MAX_COLORIZE_LEN]; + uint8_t syntaxBuf[MAX_COLORIZE_LEN]; + 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) { - lineSelLo = selLo - lineOff; - lineSelHi = selHi - lineOff; - if (lineSelLo < 0) { lineSelLo = 0; } + 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 + } } } @@ -1900,23 +2110,23 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Before selection if (drawStart < vSelLo) { if (hasSyntax) { - drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, colors); + 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, buf + lineOff + drawStart, vSelLo - drawStart, fg, bg, true); + 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, buf + lineOff + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); + 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, buf + lineOff + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, colors); + 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, buf + lineOff + vSelHi, drawEnd - vSelHi, fg, bg, true); + drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true); } } @@ -1938,9 +2148,9 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // No selection on this line if (drawStart < drawEnd) { if (hasSyntax) { - drawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, buf + lineOff + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, colors); + 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, buf + lineOff + drawStart, drawEnd - drawStart, fg, bg, true); + drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true); } } } @@ -1957,8 +2167,14 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Draw cursor (blinks at same rate as terminal cursor) if (w == sFocusedWidget && sCursorBlinkOn) { - int32_t curDrawCol = ta->cursorCol - ta->scrollCol; - int32_t curDrawRow = ta->cursorRow - ta->scrollRow; + 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; @@ -2431,8 +2647,8 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3 ta->scrollRow++; } - int32_t clickRow = ta->scrollRow + relY / font->charHeight; - int32_t clickCol = ta->scrollCol + relX / font->charWidth; + int32_t clickRow = ta->scrollRow + relY / font->charHeight; + int32_t clickVisCol = ta->scrollCol + relX / font->charWidth; if (clickRow < 0) { clickRow = 0; @@ -2442,11 +2658,14 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3 clickRow = totalLines - 1; } - if (clickCol < 0) { - clickCol = 0; + if (clickVisCol < 0) { + clickVisCol = 0; } - int32_t lineL = textAreaLineLen(ta->buf, ta->len, clickRow); + 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; @@ -2631,8 +2850,8 @@ void wgtTextAreaGoToLine(WidgetT *w, int32_t line) { ta->desiredCol = 0; // Select the entire line for visual highlight - int32_t lineStart = textAreaLineStart(ta->buf, ta->len, row); - int32_t lineL = textAreaLineLen(ta->buf, ta->len, row); + int32_t lineStart = textAreaLineStartCached(w, row); + int32_t lineL = textAreaLineLenCached(w, row); ta->selAnchor = lineStart; ta->selCursor = lineStart + lineL;