TextArea now supports actual tab characters. Added additional data cached to improve performance.

This commit is contained in:
Scott Duensing 2026-04-06 21:37:16 -05:00
parent 2a2a386592
commit 7ec2aead8f

View file

@ -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;