2502 lines
78 KiB
C
2502 lines
78 KiB
C
// The MIT License (MIT)
|
|
//
|
|
// Copyright (C) 2026 Scott Duensing
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
|
|
// textHelp.c -- Shared text editing infrastructure
|
|
//
|
|
// Provides clipboard, multi-click detection, word boundary logic,
|
|
// cross-widget selection clearing, and the single-line text editing
|
|
// engine. Shared across multiple widget DXEs (TextInput, TextArea,
|
|
// ComboBox, Spinner, AnsiTerm) via the texthelp.lib DXE.
|
|
|
|
#include "textHelp.h"
|
|
|
|
#include <ctype.h>
|
|
#include <time.h>
|
|
|
|
// ============================================================
|
|
// Static state
|
|
// ============================================================
|
|
|
|
// Cursor blink state
|
|
#define CURSOR_BLINK_MS 250
|
|
static clock_t sCursorBlinkTime = 0;
|
|
|
|
// Slack added to line cache capacity when growing. Bigger values
|
|
// mean fewer reallocs at the cost of wasted space; 256 empirically
|
|
// keeps realloc traffic low on medium-sized file edits.
|
|
#define TEXT_EDIT_LINE_CAP_GROWTH 256
|
|
|
|
// Default tab width when a cache has been left at 0 (callers should
|
|
// always initialize with textEditLineCacheInit, but guard defensively).
|
|
#define TEXT_EDIT_DEFAULT_TAB_WIDTH 3
|
|
|
|
// Track the widget that last had an active selection so we can
|
|
// clear it in O(1) instead of walking every widget in every window.
|
|
static WidgetT *sLastSelectedWidget = NULL;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
void clearOtherSelections(WidgetT *except);
|
|
static bool clearSelectionOnWidget(WidgetT *w);
|
|
bool isWordChar(char c);
|
|
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd);
|
|
void textEditDrawColorizedText(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 uint32_t *customColors);
|
|
void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol);
|
|
bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
|
|
int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize);
|
|
void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor);
|
|
void textEditLineCacheDirty(TextEditLineCacheT *lc);
|
|
void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len);
|
|
void textEditLineCacheFree(TextEditLineCacheT *lc);
|
|
void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth);
|
|
void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen);
|
|
void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen);
|
|
static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len);
|
|
void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth);
|
|
void textEditMultiFree(TextEditMultiT *te);
|
|
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth);
|
|
int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len);
|
|
int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
|
|
int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row);
|
|
int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len);
|
|
void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col);
|
|
int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor);
|
|
int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col);
|
|
void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize);
|
|
uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom);
|
|
int32_t textEditVisualCol(const char *buf, int32_t lineStart, int32_t off, int32_t tabW);
|
|
int32_t textEditVisualColToOff(const char *buf, int32_t len, int32_t lineStart, int32_t targetVC, int32_t tabW);
|
|
static void textHelpInit(void) __attribute__((constructor));
|
|
void wgtUpdateCursorBlink(void);
|
|
void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd);
|
|
void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect);
|
|
void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor);
|
|
void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor);
|
|
void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts);
|
|
void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks);
|
|
void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll);
|
|
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff);
|
|
int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff);
|
|
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth);
|
|
void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX);
|
|
int32_t wordBoundaryLeft(const char *buf, int32_t pos);
|
|
int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos);
|
|
int32_t wordEnd(const char *buf, int32_t len, int32_t pos);
|
|
int32_t wordStart(const char *buf, int32_t pos);
|
|
|
|
|
|
// Clears selection on the previously-selected widget (if different
|
|
// from the newly-focused one). Validates that the previous widget's
|
|
// window is still in the window stack before touching it -- the
|
|
// window may have been closed since sLastSelectedWidget was set.
|
|
// If the previous widget was in a different window, that window
|
|
// gets a full repaint to clear the stale selection highlight.
|
|
void clearOtherSelections(WidgetT *except) {
|
|
if (!except || !except->window || !except->window->widgetRoot) {
|
|
return;
|
|
}
|
|
|
|
WidgetT *prev = sLastSelectedWidget;
|
|
sLastSelectedWidget = except;
|
|
|
|
if (!prev || prev == except) {
|
|
return;
|
|
}
|
|
|
|
// Verify the widget is still alive (its window still in the stack)
|
|
WindowT *prevWin = prev->window;
|
|
|
|
if (!prevWin) {
|
|
return;
|
|
}
|
|
|
|
AppContextT *ctx = wgtGetContext(except);
|
|
|
|
if (!ctx) {
|
|
return;
|
|
}
|
|
|
|
bool found = false;
|
|
|
|
for (int32_t i = 0; i < ctx->stack.count; i++) {
|
|
if (ctx->stack.windows[i] == prevWin) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
return;
|
|
}
|
|
|
|
if (clearSelectionOnWidget(prev) && prevWin != except->window) {
|
|
dvxInvalidateWindow(ctx, prevWin);
|
|
}
|
|
}
|
|
|
|
|
|
static bool clearSelectionOnWidget(WidgetT *w) {
|
|
return wclsClearSelection(w);
|
|
}
|
|
|
|
|
|
bool isWordChar(char c) {
|
|
return isalnum((unsigned char)c) || c == '_';
|
|
}
|
|
|
|
|
|
static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) {
|
|
int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd;
|
|
int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart;
|
|
|
|
if (lo < 0) {
|
|
lo = 0;
|
|
}
|
|
|
|
if (hi > *pLen) {
|
|
hi = *pLen;
|
|
}
|
|
|
|
if (lo >= hi) {
|
|
*pSelStart = -1;
|
|
*pSelEnd = -1;
|
|
return;
|
|
}
|
|
|
|
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
|
*pLen -= (hi - lo);
|
|
*pCursor = lo;
|
|
*pSelStart = -1;
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
|
|
// Paint `text` as a run of single-color regions defined by
|
|
// syntaxColors[textOff..textOff+len). Coalesces adjacent same-color
|
|
// bytes to minimize drawTextN calls.
|
|
void textEditDrawColorizedText(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 uint32_t *customColors) {
|
|
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 = textEditSyntaxColor(d, curColor, defaultFg, customColors);
|
|
drawTextN(d, ops, font, x + runStart * font->charWidth, y, text + runStart, runEnd - runStart, fg, bg, true);
|
|
runStart = runEnd;
|
|
}
|
|
}
|
|
|
|
|
|
void textEditEnsureVisible(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, int32_t tabW, int32_t visRows, int32_t visCols, int32_t *pScrollRow, int32_t *pScrollCol) {
|
|
int32_t curLineOff = textEditLineStart(lc, buf, len, cursorRow);
|
|
int32_t curOff = curLineOff + cursorCol;
|
|
|
|
if (curOff > len) {
|
|
curOff = len;
|
|
}
|
|
|
|
int32_t col = textEditVisualCol(buf, curLineOff, curOff, tabW);
|
|
|
|
if (cursorRow < *pScrollRow) {
|
|
*pScrollRow = cursorRow;
|
|
}
|
|
|
|
if (cursorRow >= *pScrollRow + visRows) {
|
|
*pScrollRow = cursorRow - visRows + 1;
|
|
}
|
|
|
|
if (col < *pScrollCol) {
|
|
*pScrollCol = col;
|
|
}
|
|
|
|
if (col >= *pScrollCol + visCols) {
|
|
*pScrollCol = col - visCols + 1;
|
|
}
|
|
}
|
|
|
|
|
|
// Searches forward (or backward) from the cursor for `needle`. On
|
|
// match, selects it and moves the cursor to match start (forward)
|
|
// or match end (backward). No wrap-around.
|
|
bool textEditFindNext(TextEditLineCacheT *lc, const char *buf, int32_t len, const char *needle, bool caseSensitive, bool forward, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor) {
|
|
if (!needle || !needle[0]) {
|
|
return false;
|
|
}
|
|
|
|
int32_t needleLen = strlen(needle);
|
|
|
|
if (needleLen > len) {
|
|
return false;
|
|
}
|
|
|
|
int32_t cursorByte = textEditRowColToOff(lc, buf, len, *pCursorRow, *pCursorCol);
|
|
int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1;
|
|
int32_t searchLen = len - needleLen + 1; // >= 1 given the needleLen > len check above
|
|
int32_t count = forward ? (searchLen - startPos) : (startPos + 1);
|
|
|
|
if (count <= 0) {
|
|
return false;
|
|
}
|
|
|
|
for (int32_t attempt = 0; attempt < count; attempt++) {
|
|
int32_t pos = forward ? startPos + attempt : startPos - attempt;
|
|
|
|
if (pos < 0 || pos >= searchLen) {
|
|
break;
|
|
}
|
|
|
|
bool match;
|
|
|
|
if (caseSensitive) {
|
|
match = (memcmp(buf + pos, needle, needleLen) == 0);
|
|
} else {
|
|
match = (strncasecmp(buf + pos, needle, needleLen) == 0);
|
|
}
|
|
|
|
if (match) {
|
|
*pSelAnchor = pos;
|
|
*pSelCursor = pos + needleLen;
|
|
|
|
int32_t cursorOff = forward ? pos : pos + needleLen;
|
|
textEditOffToRowCol(lc, buf, len, cursorOff, pCursorRow, pCursorCol);
|
|
*pDesiredCol = *pCursorCol;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// Writes the word under the cursor (if any) into out and returns
|
|
// its length. Empty string if the cursor is not on a word.
|
|
int32_t textEditGetWordAtCursor(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t cursorRow, int32_t cursorCol, char *out, int32_t outSize) {
|
|
out[0] = '\0';
|
|
|
|
if (!buf || len == 0 || outSize < 2) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t off = textEditRowColToOff(lc, buf, len, cursorRow, cursorCol);
|
|
int32_t ws = wordStart(buf, off);
|
|
int32_t we = wordEnd(buf, len, off);
|
|
int32_t wdLen = we - ws;
|
|
|
|
if (wdLen <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (wdLen >= outSize) {
|
|
wdLen = outSize - 1;
|
|
}
|
|
|
|
memcpy(out, buf + ws, wdLen);
|
|
out[wdLen] = '\0';
|
|
return wdLen;
|
|
}
|
|
|
|
|
|
// Jumps to the given 1-based line, selects the whole line, and
|
|
// scrolls so the line sits ~1/4 from the top of the visible area.
|
|
void textEditGoToLine(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t line, int32_t visRows, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor) {
|
|
int32_t totalLines = textEditLineCount(lc, buf, len);
|
|
int32_t row = line - 1;
|
|
|
|
if (row < 0) {
|
|
row = 0;
|
|
}
|
|
|
|
if (row >= totalLines) {
|
|
row = totalLines - 1;
|
|
}
|
|
|
|
*pCursorRow = row;
|
|
*pCursorCol = 0;
|
|
*pDesiredCol = 0;
|
|
|
|
int32_t lineStart = textEditLineStart(lc, buf, len, row);
|
|
int32_t lineL = textEditLineLen(lc, buf, len, row);
|
|
*pSelAnchor = lineStart;
|
|
*pSelCursor = lineStart + lineL;
|
|
|
|
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;
|
|
}
|
|
|
|
*pScrollRow = targetScroll;
|
|
*pScrollCol = 0;
|
|
}
|
|
|
|
|
|
void textEditLineCacheDirty(TextEditLineCacheT *lc) {
|
|
lc->cachedLines = -1;
|
|
lc->cachedMaxLL = -1;
|
|
}
|
|
|
|
|
|
void textEditLineCacheEnsure(TextEditLineCacheT *lc, const char *buf, int32_t len) {
|
|
if (lc->cachedLines < 0) {
|
|
textEditLineCacheRebuild(lc, buf, len);
|
|
}
|
|
}
|
|
|
|
|
|
void textEditLineCacheFree(TextEditLineCacheT *lc) {
|
|
free(lc->lineOffsets);
|
|
free(lc->lineVisLens);
|
|
lc->lineOffsets = NULL;
|
|
lc->lineVisLens = NULL;
|
|
lc->lineOffsetCap = 0;
|
|
lc->lineVisLenCap = 0;
|
|
lc->cachedLines = -1;
|
|
lc->cachedMaxLL = -1;
|
|
}
|
|
|
|
|
|
void textEditLineCacheInit(TextEditLineCacheT *lc, int32_t tabWidth) {
|
|
lc->lineOffsets = NULL;
|
|
lc->lineVisLens = NULL;
|
|
lc->lineOffsetCap = 0;
|
|
lc->lineVisLenCap = 0;
|
|
lc->cachedLines = -1;
|
|
lc->cachedMaxLL = -1;
|
|
lc->tabWidth = tabWidth > 0 ? tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
|
|
}
|
|
|
|
|
|
// Incrementally update the cache after deleting bytes at `off`.
|
|
// Caller passes the POST-delete buffer and length. Falls back to a
|
|
// full rebuild if the deletion may have spanned a newline.
|
|
void textEditLineCacheNotifyDelete(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t deleteLen) {
|
|
if (lc->cachedLines < 0 || deleteLen <= 0) {
|
|
textEditLineCacheDirty(lc);
|
|
return;
|
|
}
|
|
|
|
// We only trust the incremental path for single-byte deletes; any
|
|
// longer run could have swallowed a newline, and the post-delete
|
|
// buffer can't tell us whether it did.
|
|
if (deleteLen > 1) {
|
|
textEditLineCacheDirty(lc);
|
|
return;
|
|
}
|
|
|
|
int32_t line = 0;
|
|
|
|
for (line = lc->cachedLines - 1; line > 0; line--) {
|
|
if (lc->lineOffsets[line] <= off) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = line + 1; i <= lc->cachedLines; i++) {
|
|
lc->lineOffsets[i] -= deleteLen;
|
|
}
|
|
|
|
int32_t lineOff = lc->lineOffsets[line];
|
|
int32_t nextOff = (line + 1 <= lc->cachedLines) ? lc->lineOffsets[line + 1] : len;
|
|
|
|
if (line + 1 <= lc->cachedLines && nextOff <= lineOff) {
|
|
textEditLineCacheDirty(lc);
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = lineOff; i < nextOff && i < len; i++) {
|
|
if (buf[i] == '\n') {
|
|
if (i < nextOff - 1) {
|
|
textEditLineCacheDirty(lc);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
|
|
int32_t vc = 0;
|
|
|
|
for (int32_t i = lineOff; i < nextOff && i < len && buf[i] != '\n'; i++) {
|
|
if (buf[i] == '\t') {
|
|
vc += tabW - (vc % tabW);
|
|
} else {
|
|
vc++;
|
|
}
|
|
}
|
|
|
|
lc->lineVisLens[line] = vc;
|
|
lc->cachedMaxLL = -1;
|
|
}
|
|
|
|
|
|
// Incrementally update the cache after inserting bytes at `off`.
|
|
// Caller passes the POST-insert buffer and length. Falls back to a
|
|
// full rebuild if the insertion contains newlines.
|
|
void textEditLineCacheNotifyInsert(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t insertLen) {
|
|
if (lc->cachedLines < 0 || insertLen <= 0) {
|
|
textEditLineCacheDirty(lc);
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = off; i < off + insertLen && i < len; i++) {
|
|
if (buf[i] == '\n') {
|
|
textEditLineCacheDirty(lc);
|
|
return;
|
|
}
|
|
}
|
|
|
|
int32_t line = 0;
|
|
|
|
for (line = lc->cachedLines - 1; line > 0; line--) {
|
|
if (lc->lineOffsets[line] <= off) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = line + 1; i <= lc->cachedLines; i++) {
|
|
lc->lineOffsets[i] += insertLen;
|
|
}
|
|
|
|
int32_t lineOff = lc->lineOffsets[line];
|
|
int32_t lineEnd = (line + 1 <= lc->cachedLines) ? lc->lineOffsets[line + 1] : len;
|
|
int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
|
|
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++;
|
|
}
|
|
}
|
|
|
|
lc->lineVisLens[line] = vc;
|
|
|
|
if (vc > lc->cachedMaxLL) {
|
|
lc->cachedMaxLL = vc;
|
|
} else {
|
|
lc->cachedMaxLL = -1;
|
|
}
|
|
}
|
|
|
|
|
|
// Single O(N) scan populates line-offset table plus per-line
|
|
// visual lengths. lineOffsets[lineCount] is the buffer-end sentinel.
|
|
static void textEditLineCacheRebuild(TextEditLineCacheT *lc, const char *buf, int32_t len) {
|
|
int32_t tabW = lc->tabWidth > 0 ? lc->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
|
|
int32_t lineCount = 1;
|
|
|
|
for (int32_t i = 0; i < len; i++) {
|
|
if (buf[i] == '\n') {
|
|
lineCount++;
|
|
}
|
|
}
|
|
|
|
int32_t needed = lineCount + 1;
|
|
|
|
if (needed > lc->lineOffsetCap) {
|
|
int32_t newCap = needed + TEXT_EDIT_LINE_CAP_GROWTH;
|
|
lc->lineOffsets = (int32_t *)realloc(lc->lineOffsets, newCap * sizeof(int32_t));
|
|
lc->lineOffsetCap = newCap;
|
|
}
|
|
|
|
if (lineCount > lc->lineVisLenCap) {
|
|
int32_t newCap = lineCount + TEXT_EDIT_LINE_CAP_GROWTH;
|
|
lc->lineVisLens = (int32_t *)realloc(lc->lineVisLens, newCap * sizeof(int32_t));
|
|
lc->lineVisLenCap = newCap;
|
|
}
|
|
|
|
int32_t line = 0;
|
|
int32_t vc = 0;
|
|
int32_t maxVL = 0;
|
|
|
|
lc->lineOffsets[0] = 0;
|
|
|
|
for (int32_t i = 0; i < len; i++) {
|
|
if (buf[i] == '\n') {
|
|
lc->lineVisLens[line] = vc;
|
|
|
|
if (vc > maxVL) {
|
|
maxVL = vc;
|
|
}
|
|
|
|
line++;
|
|
lc->lineOffsets[line] = i + 1;
|
|
vc = 0;
|
|
} else if (buf[i] == '\t') {
|
|
vc += tabW - (vc % tabW);
|
|
} else {
|
|
vc++;
|
|
}
|
|
}
|
|
|
|
lc->lineVisLens[line] = vc;
|
|
|
|
if (vc > maxVL) {
|
|
maxVL = vc;
|
|
}
|
|
|
|
lc->lineOffsets[lineCount] = len;
|
|
lc->cachedLines = lineCount;
|
|
lc->cachedMaxLL = maxVL;
|
|
}
|
|
|
|
|
|
void textEditLineCacheSetTabWidth(TextEditLineCacheT *lc, int32_t tabWidth) {
|
|
int32_t newW = tabWidth > 0 ? tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
|
|
|
|
if (lc->tabWidth == newW) {
|
|
return;
|
|
}
|
|
|
|
lc->tabWidth = newW;
|
|
textEditLineCacheDirty(lc);
|
|
}
|
|
|
|
|
|
void textEditMultiFree(TextEditMultiT *te) {
|
|
free(te->buf);
|
|
free(te->undoBuf);
|
|
textEditLineCacheFree(&te->lines);
|
|
te->buf = NULL;
|
|
te->undoBuf = NULL;
|
|
te->bufSize = 0;
|
|
te->len = 0;
|
|
}
|
|
|
|
|
|
// Allocates the buf/undoBuf pair sized for maxLen characters plus
|
|
// NUL, and initializes cursor/scroll/selection/undo state. Returns
|
|
// false (leaving te->buf == NULL) if allocation fails.
|
|
bool textEditMultiInit(TextEditMultiT *te, int32_t maxLen, int32_t tabWidth) {
|
|
memset(te, 0, sizeof(*te));
|
|
|
|
int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256;
|
|
|
|
te->buf = (char *)malloc(bufSize);
|
|
te->undoBuf = (char *)malloc(bufSize);
|
|
te->bufSize = bufSize;
|
|
|
|
if (!te->buf || !te->undoBuf) {
|
|
free(te->buf);
|
|
free(te->undoBuf);
|
|
te->buf = NULL;
|
|
te->undoBuf = NULL;
|
|
return false;
|
|
}
|
|
|
|
te->buf[0] = '\0';
|
|
te->undoBuf[0] = '\0';
|
|
te->selAnchor = -1;
|
|
te->selCursor = -1;
|
|
textEditLineCacheInit(&te->lines, tabWidth);
|
|
return true;
|
|
}
|
|
|
|
|
|
int32_t textEditLineCount(TextEditLineCacheT *lc, const char *buf, int32_t len) {
|
|
textEditLineCacheEnsure(lc, buf, len);
|
|
return lc->cachedLines;
|
|
}
|
|
|
|
|
|
int32_t textEditLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row) {
|
|
textEditLineCacheEnsure(lc, buf, len);
|
|
|
|
if (row < 0 || row >= lc->cachedLines) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t start = lc->lineOffsets[row];
|
|
int32_t next = lc->lineOffsets[row + 1];
|
|
|
|
if (next > start && buf[next - 1] == '\n') {
|
|
next--;
|
|
}
|
|
|
|
return next - start;
|
|
}
|
|
|
|
|
|
int32_t textEditLineStart(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row) {
|
|
textEditLineCacheEnsure(lc, buf, len);
|
|
|
|
if (row < 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (row >= lc->cachedLines) {
|
|
return len;
|
|
}
|
|
|
|
return lc->lineOffsets[row];
|
|
}
|
|
|
|
|
|
int32_t textEditMaxLineLen(TextEditLineCacheT *lc, const char *buf, int32_t len) {
|
|
textEditLineCacheEnsure(lc, buf, len);
|
|
|
|
if (lc->cachedMaxLL < 0 && lc->cachedLines >= 0) {
|
|
int32_t maxVL = 0;
|
|
|
|
for (int32_t i = 0; i < lc->cachedLines; i++) {
|
|
if (lc->lineVisLens[i] > maxVL) {
|
|
maxVL = lc->lineVisLens[i];
|
|
}
|
|
}
|
|
|
|
lc->cachedMaxLL = maxVL;
|
|
}
|
|
|
|
return lc->cachedMaxLL;
|
|
}
|
|
|
|
|
|
void textEditOffToRowCol(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t off, int32_t *row, int32_t *col) {
|
|
textEditLineCacheEnsure(lc, buf, len);
|
|
|
|
if (!lc->lineOffsets || lc->cachedLines < 0) {
|
|
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;
|
|
return;
|
|
}
|
|
|
|
int32_t lo = 0;
|
|
int32_t hi = lc->cachedLines;
|
|
|
|
while (lo < hi) {
|
|
int32_t mid = (lo + hi + 1) / 2;
|
|
|
|
if (lc->lineOffsets[mid] <= off) {
|
|
lo = mid;
|
|
} else {
|
|
hi = mid - 1;
|
|
}
|
|
}
|
|
|
|
*row = lo;
|
|
*col = off - lc->lineOffsets[lo];
|
|
}
|
|
|
|
|
|
// Replaces every occurrence of needle with replacement. Records a
|
|
// single undo snapshot before mutating. Clamps the cursor to the
|
|
// post-replace buffer length and clears any selection. Returns the
|
|
// count of replacements made.
|
|
int32_t textEditReplaceAll(TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, const char *needle, const char *replacement, bool caseSensitive, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) {
|
|
if (!needle || !needle[0] || !replacement) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t needleLen = strlen(needle);
|
|
int32_t replLen = strlen(replacement);
|
|
int32_t delta = replLen - needleLen;
|
|
int32_t count = 0;
|
|
|
|
if (needleLen > *pLen) {
|
|
return 0;
|
|
}
|
|
|
|
textEditSaveUndo(buf, *pLen, textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol), undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
|
|
int32_t pos = 0;
|
|
|
|
while (pos + needleLen <= *pLen) {
|
|
bool match;
|
|
|
|
if (caseSensitive) {
|
|
match = (memcmp(buf + pos, needle, needleLen) == 0);
|
|
} else {
|
|
match = (strncasecmp(buf + pos, needle, needleLen) == 0);
|
|
}
|
|
|
|
if (match) {
|
|
if (*pLen + delta >= bufSize) {
|
|
break;
|
|
}
|
|
|
|
if (delta != 0) {
|
|
memmove(buf + pos + replLen, buf + pos + needleLen, *pLen - pos - needleLen);
|
|
}
|
|
|
|
memcpy(buf + pos, replacement, replLen);
|
|
*pLen += delta;
|
|
buf[*pLen] = '\0';
|
|
count++;
|
|
pos += replLen;
|
|
} else {
|
|
pos++;
|
|
}
|
|
}
|
|
|
|
if (count > 0) {
|
|
*pSelAnchor = -1;
|
|
*pSelCursor = -1;
|
|
|
|
int32_t cursorOff = textEditRowColToOff(lc, buf, *pLen, *pCursorRow, *pCursorCol);
|
|
|
|
if (cursorOff > *pLen) {
|
|
textEditOffToRowCol(lc, buf, *pLen, *pLen, pCursorRow, pCursorCol);
|
|
}
|
|
|
|
*pDesiredCol = *pCursorCol;
|
|
textEditLineCacheDirty(lc);
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
|
|
int32_t textEditRowColToOff(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t row, int32_t col) {
|
|
int32_t start = textEditLineStart(lc, buf, len, row);
|
|
int32_t lineL = textEditLineLen(lc, buf, len, row);
|
|
int32_t clamp = col < lineL ? col : lineL;
|
|
|
|
return start + clamp;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
// Resolve a syntax color index to a packed-pixel foreground color.
|
|
// Falls back to defaultFg for TEXT_SYNTAX_DEFAULT or unknown indices.
|
|
// A non-zero entry in custom[idx] (0x00RRGGBB) overrides the default.
|
|
uint32_t textEditSyntaxColor(const DisplayT *d, uint8_t idx, uint32_t defaultFg, const uint32_t *custom) {
|
|
if (idx != TEXT_SYNTAX_DEFAULT && idx < TEXT_SYNTAX_MAX && custom && custom[idx]) {
|
|
uint32_t c = custom[idx];
|
|
return packColor(d, (c >> 16) & 0xFF, (c >> 8) & 0xFF, c & 0xFF);
|
|
}
|
|
|
|
switch (idx) {
|
|
case TEXT_SYNTAX_KEYWORD: return packColor(d, 0, 0, 128);
|
|
case TEXT_SYNTAX_STRING: return packColor(d, 128, 0, 0);
|
|
case TEXT_SYNTAX_COMMENT: return packColor(d, 0, 128, 0);
|
|
case TEXT_SYNTAX_NUMBER: return packColor(d, 128, 0, 128);
|
|
case TEXT_SYNTAX_OPERATOR: return packColor(d, 128, 128, 0);
|
|
case TEXT_SYNTAX_TYPE: return packColor(d, 0, 128, 128);
|
|
default: return defaultFg;
|
|
}
|
|
}
|
|
|
|
|
|
// Tab-aware visual column from buffer offset within a line.
|
|
// tabW <= 0 means tabs are 1 column (no expansion).
|
|
int32_t textEditVisualCol(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.
|
|
int32_t textEditVisualColToOff(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 void textHelpInit(void) {
|
|
sCursorBlinkFn = wgtUpdateCursorBlink;
|
|
}
|
|
|
|
|
|
void wgtUpdateCursorBlink(void) {
|
|
clock_t now = clock();
|
|
clock_t interval = (clock_t)CURSOR_BLINK_MS * CLOCKS_PER_SEC / 1000;
|
|
|
|
if ((now - sCursorBlinkTime) >= interval) {
|
|
sCursorBlinkTime = now;
|
|
sCursorBlinkOn = !sCursorBlinkOn;
|
|
|
|
if (sFocusedWidget) {
|
|
wgtInvalidatePaint(sFocusedWidget);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Called during mouse drag to extend the selection for single-line
|
|
// text widgets. Auto-scrolls when the mouse moves past the visible
|
|
// text edges.
|
|
void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd) {
|
|
int32_t rightEdge = leftEdge + maxChars * font->charWidth;
|
|
|
|
if (vx < leftEdge && *pScrollOff > 0) {
|
|
(*pScrollOff)--;
|
|
} else if (vx >= rightEdge && *pScrollOff + maxChars < len) {
|
|
(*pScrollOff)++;
|
|
}
|
|
|
|
int32_t relX = vx - leftEdge;
|
|
int32_t charPos = relX / font->charWidth + *pScrollOff;
|
|
|
|
if (charPos < 0) {
|
|
charPos = 0;
|
|
}
|
|
|
|
if (charPos > len) {
|
|
charPos = len;
|
|
}
|
|
|
|
*pCursorPos = charPos;
|
|
*pSelEnd = charPos;
|
|
}
|
|
|
|
|
|
// Computes cursor position from pixel coordinates, handles multi-click
|
|
// (double = word select, triple = select all), and optionally starts
|
|
// drag-select.
|
|
void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect) {
|
|
int32_t relX = vx - textLeftX;
|
|
int32_t charPos = relX / font->charWidth + scrollOff;
|
|
|
|
if (charPos < 0) {
|
|
charPos = 0;
|
|
}
|
|
|
|
if (charPos > len) {
|
|
charPos = len;
|
|
}
|
|
|
|
int32_t clicks = multiClickDetect(vx, vy);
|
|
|
|
if (clicks >= 3) {
|
|
*pSelStart = 0;
|
|
*pSelEnd = len;
|
|
*pCursorPos = len;
|
|
sDragWidget = NULL;
|
|
return;
|
|
}
|
|
|
|
if (clicks == 2) {
|
|
if (wordSelect && buf) {
|
|
int32_t ws = wordStart(buf, charPos);
|
|
int32_t we = wordEnd(buf, len, charPos);
|
|
*pSelStart = ws;
|
|
*pSelEnd = we;
|
|
*pCursorPos = we;
|
|
} else {
|
|
*pSelStart = 0;
|
|
*pSelEnd = len;
|
|
*pCursorPos = len;
|
|
}
|
|
|
|
sDragWidget = NULL;
|
|
return;
|
|
}
|
|
|
|
// Single click: place cursor
|
|
*pCursorPos = charPos;
|
|
*pSelStart = charPos;
|
|
*pSelEnd = charPos;
|
|
sDragWidget = dragSelect ? w : NULL;
|
|
}
|
|
|
|
|
|
// Called during a drag to auto-scroll when the mouse moves past the
|
|
// top or bottom of the visible area, and to extend the selection to
|
|
// the new cursor position.
|
|
void widgetTextEditMultiDragUpdateArea(TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t *pScrollRow, int32_t scrollCol, int32_t visRows, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelCursor) {
|
|
int32_t relX = vx - textX;
|
|
int32_t relY = vy - textY;
|
|
int32_t totalLines = textEditLineCount(lc, buf, len);
|
|
|
|
if (relY < 0 && *pScrollRow > 0) {
|
|
(*pScrollRow)--;
|
|
} else if (relY >= visRows * font->charHeight && *pScrollRow < totalLines - visRows) {
|
|
(*pScrollRow)++;
|
|
}
|
|
|
|
int32_t clickRow = *pScrollRow + relY / font->charHeight;
|
|
int32_t clickVisCol = scrollCol + relX / font->charWidth;
|
|
|
|
if (clickRow < 0) {
|
|
clickRow = 0;
|
|
}
|
|
|
|
if (clickRow >= totalLines) {
|
|
clickRow = totalLines - 1;
|
|
}
|
|
|
|
if (clickVisCol < 0) {
|
|
clickVisCol = 0;
|
|
}
|
|
|
|
int32_t dragLineStart = textEditLineStart(lc, buf, len, clickRow);
|
|
int32_t dragByteOff = textEditVisualColToOff(buf, len, dragLineStart, clickVisCol, tabW);
|
|
int32_t clickCol = dragByteOff - dragLineStart;
|
|
int32_t lineL = textEditLineLen(lc, buf, len, clickRow);
|
|
|
|
if (clickCol > lineL) {
|
|
clickCol = lineL;
|
|
}
|
|
|
|
*pCursorRow = clickRow;
|
|
*pCursorCol = clickCol;
|
|
*pDesiredCol = clickCol;
|
|
*pSelCursor = textEditRowColToOff(lc, buf, len, clickRow, clickCol);
|
|
}
|
|
|
|
|
|
// Handle a mouse click on the text content area. Caller has already
|
|
// excluded scrollbar and gutter clicks. Single click places cursor
|
|
// and starts a drag anchor; double-click selects the word under
|
|
// the cursor; triple-click selects the whole line.
|
|
void widgetTextEditMultiMouseClick(WidgetT *w, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t vx, int32_t vy, int32_t textX, int32_t textY, const BitmapFontT *font, int32_t scrollRow, int32_t scrollCol, int32_t tabW, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pSelAnchor, int32_t *pSelCursor) {
|
|
int32_t totalLines = textEditLineCount(lc, buf, len);
|
|
int32_t relX = vx - textX;
|
|
int32_t relY = vy - textY;
|
|
int32_t clickRow = scrollRow + relY / font->charHeight;
|
|
int32_t clickVisCol = scrollCol + relX / font->charWidth;
|
|
|
|
if (clickRow < 0) {
|
|
clickRow = 0;
|
|
}
|
|
|
|
if (clickRow >= totalLines) {
|
|
clickRow = totalLines - 1;
|
|
}
|
|
|
|
if (clickVisCol < 0) {
|
|
clickVisCol = 0;
|
|
}
|
|
|
|
int32_t clkLineStart = textEditLineStart(lc, buf, len, clickRow);
|
|
int32_t clkByteOff = textEditVisualColToOff(buf, len, clkLineStart, clickVisCol, tabW);
|
|
int32_t clickCol = clkByteOff - clkLineStart;
|
|
int32_t lineL = textEditLineLen(lc, buf, len, clickRow);
|
|
|
|
if (clickCol > lineL) {
|
|
clickCol = lineL;
|
|
}
|
|
|
|
int32_t clicks = multiClickDetect(vx, vy);
|
|
|
|
if (clicks >= 3) {
|
|
int32_t lineStart = textEditRowColToOff(lc, buf, len, clickRow, 0);
|
|
int32_t lineEnd = textEditRowColToOff(lc, buf, len, clickRow, lineL);
|
|
|
|
*pCursorRow = clickRow;
|
|
*pCursorCol = lineL;
|
|
*pDesiredCol = lineL;
|
|
*pSelAnchor = lineStart;
|
|
*pSelCursor = lineEnd;
|
|
sDragWidget = NULL;
|
|
return;
|
|
}
|
|
|
|
if (clicks == 2 && buf) {
|
|
int32_t off = textEditRowColToOff(lc, buf, len, clickRow, clickCol);
|
|
int32_t ws = wordStart(buf, off);
|
|
int32_t we = wordEnd(buf, len, off);
|
|
int32_t weRow;
|
|
int32_t weCol;
|
|
textEditOffToRowCol(lc, buf, len, we, &weRow, &weCol);
|
|
|
|
*pCursorRow = weRow;
|
|
*pCursorCol = weCol;
|
|
*pDesiredCol = weCol;
|
|
*pSelAnchor = ws;
|
|
*pSelCursor = we;
|
|
sDragWidget = NULL;
|
|
return;
|
|
}
|
|
|
|
*pCursorRow = clickRow;
|
|
*pCursorCol = clickCol;
|
|
*pDesiredCol = clickCol;
|
|
|
|
int32_t anchorOff = textEditRowColToOff(lc, buf, len, clickRow, clickCol);
|
|
*pSelAnchor = anchorOff;
|
|
*pSelCursor = anchorOff;
|
|
sDragWidget = w;
|
|
}
|
|
|
|
|
|
// Core multi-line text editing engine. Parameterized on state
|
|
// pointers so that the widget's fields remain its own; the library
|
|
// never dereferences TextAreaDataT or similar.
|
|
void widgetTextEditMultiOnKey(WidgetT *w, int32_t key, int32_t mod, TextEditLineCacheT *lc, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursorRow, int32_t *pCursorCol, int32_t *pDesiredCol, int32_t *pScrollRow, int32_t *pScrollCol, int32_t *pSelAnchor, int32_t *pSelCursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t visRows, int32_t visCols, const TextEditMultiOptionsT *opts) {
|
|
int32_t *pRow = pCursorRow;
|
|
int32_t *pCol = pCursorCol;
|
|
int32_t *pSA = pSelAnchor;
|
|
int32_t *pSC = pSelCursor;
|
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
|
int32_t tabW = opts->tabWidth > 0 ? opts->tabWidth : TEXT_EDIT_DEFAULT_TAB_WIDTH;
|
|
|
|
#define CUR_OFF() textEditRowColToOff(lc, buf, *pLen, *pRow, *pCol)
|
|
#define ENSURE_VIS() textEditEnsureVisible(lc, buf, *pLen, *pRow, *pCol, tabW, visRows, visCols, pScrollRow, pScrollCol)
|
|
#define FROM_OFF(o) textEditOffToRowCol(lc, buf, *pLen, (o), pRow, pCol)
|
|
#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)
|
|
|
|
if (HAS_SEL()) {
|
|
if (*pSA > *pLen) {
|
|
*pSA = *pLen;
|
|
}
|
|
|
|
if (*pSC > *pLen) {
|
|
*pSC = *pLen;
|
|
}
|
|
}
|
|
|
|
// Ctrl+A -- select all
|
|
if (key == 1) {
|
|
*pSA = 0;
|
|
*pSC = *pLen;
|
|
FROM_OFF(*pLen);
|
|
*pDesiredCol = *pCol;
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Ctrl+C -- copy
|
|
if (key == KEY_CTRL_C) {
|
|
if (HAS_SEL()) {
|
|
clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (opts->readOnly) {
|
|
goto navigation;
|
|
}
|
|
|
|
// Ctrl+V -- paste
|
|
if (key == KEY_CTRL_V) {
|
|
int32_t clipLen = 0;
|
|
const char *clip = clipboardGet(&clipLen);
|
|
|
|
if (clipLen > 0) {
|
|
textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, 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);
|
|
FROM_OFF(lo);
|
|
*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;
|
|
FROM_OFF(off + paste);
|
|
*pDesiredCol = *pCol;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
textEditLineCacheDirty(lc);
|
|
}
|
|
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Ctrl+X -- cut
|
|
if (key == KEY_CTRL_X) {
|
|
if (HAS_SEL()) {
|
|
clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO());
|
|
textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
|
|
int32_t lo = SEL_LO();
|
|
int32_t hi = SEL_HI();
|
|
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
|
*pLen -= (hi - lo);
|
|
FROM_OFF(lo);
|
|
*pSA = -1;
|
|
*pSC = -1;
|
|
*pDesiredCol = *pCol;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
textEditLineCacheDirty(lc);
|
|
}
|
|
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Ctrl+Z -- undo (single-slot swap; gives you one level of redo)
|
|
if (key == KEY_CTRL_Z) {
|
|
if (undoBuf && *pUndoLen >= 0) {
|
|
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 = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1;
|
|
memcpy(buf, undoBuf, restLen);
|
|
buf[restLen] = '\0';
|
|
*pLen = restLen;
|
|
|
|
int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
|
|
memcpy(undoBuf, tmpBuf, saveLen);
|
|
undoBuf[saveLen] = '\0';
|
|
*pUndoLen = saveLen;
|
|
*pUndoCursor = tmpCursor;
|
|
|
|
int32_t restoreOff = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;
|
|
*pUndoCursor = tmpCursor;
|
|
FROM_OFF(restoreOff);
|
|
*pDesiredCol = *pCol;
|
|
*pSA = -1;
|
|
*pSC = -1;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
textEditLineCacheDirty(lc);
|
|
}
|
|
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Enter -- insert newline (optionally copies leading whitespace)
|
|
if (key == 0x0D) {
|
|
if (*pLen < bufSize - 1) {
|
|
textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, 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);
|
|
FROM_OFF(lo);
|
|
*pSA = -1;
|
|
*pSC = -1;
|
|
}
|
|
|
|
int32_t off = CUR_OFF();
|
|
int32_t indent = 0;
|
|
char indentBuf[64];
|
|
|
|
if (opts->autoIndent) {
|
|
int32_t lineStart = textEditLineStart(lc, 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;
|
|
*pDesiredCol = indent;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
textEditLineCacheDirty(lc);
|
|
}
|
|
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Backspace
|
|
if (key == 8) {
|
|
if (HAS_SEL()) {
|
|
textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
int32_t lo = SEL_LO();
|
|
int32_t hi = SEL_HI();
|
|
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
|
*pLen -= (hi - lo);
|
|
FROM_OFF(lo);
|
|
*pSA = -1;
|
|
*pSC = -1;
|
|
*pDesiredCol = *pCol;
|
|
textEditLineCacheDirty(lc);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
} else {
|
|
int32_t off = CUR_OFF();
|
|
|
|
if (off > 0) {
|
|
char deleted = buf[off - 1];
|
|
textEditSaveUndo(buf, *pLen, off, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
memmove(buf + off - 1, buf + off, *pLen - off + 1);
|
|
(*pLen)--;
|
|
FROM_OFF(off - 1);
|
|
*pDesiredCol = *pCol;
|
|
|
|
if (deleted == '\n') {
|
|
textEditLineCacheDirty(lc);
|
|
} else {
|
|
textEditLineCacheNotifyDelete(lc, buf, *pLen, off - 1, 1);
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
}
|
|
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Delete
|
|
if (key == KEY_DELETE) {
|
|
if (HAS_SEL()) {
|
|
textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
int32_t lo = SEL_LO();
|
|
int32_t hi = SEL_HI();
|
|
memmove(buf + lo, buf + hi, *pLen - hi + 1);
|
|
*pLen -= (hi - lo);
|
|
FROM_OFF(lo);
|
|
*pSA = -1;
|
|
*pSC = -1;
|
|
*pDesiredCol = *pCol;
|
|
textEditLineCacheDirty(lc);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
} else {
|
|
int32_t off = CUR_OFF();
|
|
|
|
if (off < *pLen) {
|
|
char deleted = buf[off];
|
|
textEditSaveUndo(buf, *pLen, off, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
memmove(buf + off, buf + off + 1, *pLen - off);
|
|
(*pLen)--;
|
|
|
|
if (deleted == '\n') {
|
|
textEditLineCacheDirty(lc);
|
|
} else {
|
|
textEditLineCacheNotifyDelete(lc, buf, *pLen, off, 1);
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
}
|
|
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
navigation: {
|
|
int32_t totalLines = textEditLineCount(lc, buf, *pLen);
|
|
|
|
// Left arrow
|
|
if (key == KEY_LEFT) {
|
|
SEL_BEGIN();
|
|
int32_t off = CUR_OFF();
|
|
|
|
if (off > 0) {
|
|
FROM_OFF(off - 1);
|
|
}
|
|
|
|
*pDesiredCol = *pCol;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Right arrow
|
|
if (key == KEY_RIGHT) {
|
|
SEL_BEGIN();
|
|
int32_t off = CUR_OFF();
|
|
|
|
if (off < *pLen) {
|
|
FROM_OFF(off + 1);
|
|
}
|
|
|
|
*pDesiredCol = *pCol;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Ctrl+Left -- word left
|
|
if (key == (0x73 | 0x100)) {
|
|
SEL_BEGIN();
|
|
int32_t off = CUR_OFF();
|
|
int32_t newOff = wordBoundaryLeft(buf, off);
|
|
FROM_OFF(newOff);
|
|
*pDesiredCol = *pCol;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
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);
|
|
FROM_OFF(newOff);
|
|
*pDesiredCol = *pCol;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Up arrow
|
|
if (key == KEY_UP) {
|
|
SEL_BEGIN();
|
|
|
|
if (*pRow > 0) {
|
|
(*pRow)--;
|
|
int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
|
|
*pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
|
|
}
|
|
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Down arrow
|
|
if (key == KEY_DOWN) {
|
|
SEL_BEGIN();
|
|
|
|
if (*pRow < totalLines - 1) {
|
|
(*pRow)++;
|
|
int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
|
|
*pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
|
|
}
|
|
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Home
|
|
if (key == KEY_HOME) {
|
|
SEL_BEGIN();
|
|
*pCol = 0;
|
|
*pDesiredCol = 0;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// End
|
|
if (key == KEY_END) {
|
|
SEL_BEGIN();
|
|
*pCol = textEditLineLen(lc, buf, *pLen, *pRow);
|
|
*pDesiredCol = *pCol;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Page Up
|
|
if (key == KEY_PGUP) {
|
|
SEL_BEGIN();
|
|
*pRow -= visRows;
|
|
|
|
if (*pRow < 0) {
|
|
*pRow = 0;
|
|
}
|
|
|
|
int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
|
|
*pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Page Down
|
|
if (key == KEY_PGDN) {
|
|
SEL_BEGIN();
|
|
*pRow += visRows;
|
|
|
|
if (*pRow >= totalLines) {
|
|
*pRow = totalLines - 1;
|
|
}
|
|
|
|
int32_t lineL = textEditLineLen(lc, buf, *pLen, *pRow);
|
|
*pCol = *pDesiredCol < lineL ? *pDesiredCol : lineL;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Ctrl+Home
|
|
if (key == (0x77 | 0x100)) {
|
|
SEL_BEGIN();
|
|
*pRow = 0;
|
|
*pCol = 0;
|
|
*pDesiredCol = 0;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
// Ctrl+End
|
|
if (key == (0x75 | 0x100)) {
|
|
SEL_BEGIN();
|
|
FROM_OFF(*pLen);
|
|
*pDesiredCol = *pCol;
|
|
SEL_END();
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Tab key
|
|
if (key == 9 && opts->captureTabs && !opts->readOnly) {
|
|
if (*pLen < bufSize - 1) {
|
|
textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, 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);
|
|
FROM_OFF(lo);
|
|
*pSA = -1;
|
|
*pSC = -1;
|
|
}
|
|
|
|
int32_t off = CUR_OFF();
|
|
|
|
if (opts->useTabChar) {
|
|
if (*pLen < bufSize - 1) {
|
|
memmove(buf + off + 1, buf + off, *pLen - off + 1);
|
|
buf[off] = '\t';
|
|
(*pLen)++;
|
|
(*pCol)++;
|
|
*pDesiredCol = *pCol;
|
|
}
|
|
} else {
|
|
int32_t spaces = tabW - (*pCol % tabW);
|
|
|
|
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)++;
|
|
}
|
|
|
|
*pDesiredCol = *pCol;
|
|
}
|
|
|
|
textEditLineCacheDirty(lc);
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Printable character
|
|
if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST && !opts->readOnly) {
|
|
if (*pLen < bufSize - 1) {
|
|
textEditSaveUndo(buf, *pLen, CUR_OFF(), undoBuf, pUndoLen, pUndoCursor, 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);
|
|
FROM_OFF(lo);
|
|
*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)++;
|
|
*pDesiredCol = *pCol;
|
|
textEditLineCacheNotifyInsert(lc, buf, *pLen, off, 1);
|
|
} else {
|
|
textEditLineCacheDirty(lc);
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
ENSURE_VIS();
|
|
wgtInvalidatePaint(w);
|
|
return;
|
|
}
|
|
|
|
#undef CUR_OFF
|
|
#undef ENSURE_VIS
|
|
#undef FROM_OFF
|
|
#undef SEL_BEGIN
|
|
#undef SEL_END
|
|
#undef HAS_SEL
|
|
#undef SEL_LO
|
|
#undef SEL_HI
|
|
}
|
|
|
|
|
|
// Paints the text content of a multi-line editor inside the box at
|
|
// (textX, textY, innerW, visRows*charHeight). Handles tab expansion,
|
|
// optional per-character syntax coloring, selection highlighting,
|
|
// past-EOL selection fill, cursor draw, and optional per-line
|
|
// background overrides via hooks->lineDecorator.
|
|
//
|
|
// Does NOT touch the surrounding border, gutter, scrollbars, or
|
|
// focus rect -- those are caller concerns (widget chrome).
|
|
//
|
|
// Selection offsets are byte offsets into buf. selAnchor/selCursor
|
|
// may be -1 to indicate no selection; unequal values with both >= 0
|
|
// produce a highlighted range.
|
|
void widgetTextEditMultiPaintArea(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, TextEditLineCacheT *lc, const char *buf, int32_t len, int32_t textX, int32_t textY, int32_t innerW, int32_t visCols, int32_t visRows, int32_t scrollRow, int32_t scrollCol, int32_t cursorRow, int32_t cursorCol, int32_t selAnchor, int32_t selCursor, int32_t tabW, uint32_t fg, uint32_t bg, bool showCursor, const TextEditPaintHooksT *hooks) {
|
|
int32_t totalLines = textEditLineCount(lc, buf, len);
|
|
int32_t selLo = -1;
|
|
int32_t selHi = -1;
|
|
|
|
if (selAnchor >= 0 && selCursor >= 0 && selAnchor != selCursor) {
|
|
selLo = selAnchor < selCursor ? selAnchor : selCursor;
|
|
selHi = selAnchor < selCursor ? selCursor : selAnchor;
|
|
}
|
|
|
|
int32_t lineOff = textEditLineStart(lc, buf, len, scrollRow);
|
|
int32_t scratchLen = hooks ? hooks->scratchLen : 0;
|
|
uint8_t *rawSyntax = hooks ? hooks->rawSyntax : NULL;
|
|
char *expandBuf = hooks ? hooks->expandBuf : NULL;
|
|
uint8_t *syntaxBuf = hooks ? hooks->syntaxBuf : NULL;
|
|
|
|
for (int32_t i = 0; i < visRows; i++) {
|
|
int32_t row = scrollRow + i;
|
|
|
|
if (row >= totalLines) {
|
|
break;
|
|
}
|
|
|
|
int32_t lineL = textEditLineLen(lc, buf, len, row);
|
|
int32_t drawY = textY + i * font->charHeight;
|
|
|
|
uint32_t lineBg = bg;
|
|
uint32_t gutterColor = 0;
|
|
|
|
if (hooks && hooks->lineDecorator) {
|
|
uint32_t decBg = hooks->lineDecorator(row + 1, &gutterColor, hooks->lineDecoratorCtx);
|
|
|
|
if (decBg) {
|
|
lineBg = decBg;
|
|
}
|
|
|
|
rectFill(d, ops, textX, drawY, innerW, font->charHeight, lineBg);
|
|
}
|
|
|
|
uint32_t savedBg = bg;
|
|
bg = lineBg;
|
|
|
|
bool hasSyntax = false;
|
|
|
|
if (hooks && hooks->colorize && lineL > 0 && rawSyntax && scratchLen > 0) {
|
|
int32_t colorLen = lineL < scratchLen ? lineL : scratchLen;
|
|
memset(rawSyntax, 0, colorLen);
|
|
hooks->colorize(buf + lineOff, colorLen, rawSyntax, hooks->colorizeCtx);
|
|
hasSyntax = true;
|
|
}
|
|
|
|
int32_t expandLen = 0;
|
|
int32_t vc = 0;
|
|
|
|
if (expandBuf && syntaxBuf && scratchLen > 0) {
|
|
for (int32_t j = 0; j < lineL && expandLen < scratchLen - tabW; j++) {
|
|
if (buf[lineOff + j] == '\t') {
|
|
int32_t spaces = tabW - (vc % tabW);
|
|
uint8_t sc = hasSyntax && j < scratchLen ? rawSyntax[j] : 0;
|
|
|
|
for (int32_t s = 0; s < spaces && expandLen < scratchLen; s++) {
|
|
expandBuf[expandLen] = ' ';
|
|
syntaxBuf[expandLen] = sc;
|
|
expandLen++;
|
|
vc++;
|
|
}
|
|
} else {
|
|
expandBuf[expandLen] = buf[lineOff + j];
|
|
syntaxBuf[expandLen] = hasSyntax && j < scratchLen ? rawSyntax[j] : 0;
|
|
expandLen++;
|
|
vc++;
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t visStart = scrollCol;
|
|
int32_t visEnd = scrollCol + visCols;
|
|
int32_t textEnd = expandLen;
|
|
int32_t drawStart = visStart < textEnd ? visStart : textEnd;
|
|
int32_t drawEnd = visEnd < textEnd ? visEnd : textEnd;
|
|
|
|
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 = textEditVisualCol(buf, lineOff, lineOff + byteSelLo, tabW);
|
|
lineSelHi = textEditVisualCol(buf, lineOff, lineOff + (byteSelHi < lineL ? byteSelHi : lineL), tabW);
|
|
|
|
if (byteSelHi > lineL) {
|
|
lineSelHi = expandLen + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
const uint32_t *custom = hooks ? hooks->customSyntaxColors : NULL;
|
|
|
|
if (lineSelLo >= 0 && lineSelLo < lineSelHi) {
|
|
int32_t vSelLo = lineSelLo < drawStart ? drawStart : lineSelLo;
|
|
int32_t vSelHi = lineSelHi < drawEnd ? lineSelHi : drawEnd;
|
|
|
|
if (vSelLo > vSelHi) {
|
|
vSelLo = vSelHi;
|
|
}
|
|
|
|
if (drawStart < vSelLo) {
|
|
if (hasSyntax) {
|
|
textEditDrawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, syntaxBuf, drawStart, fg, bg, custom);
|
|
} else {
|
|
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, vSelLo - drawStart, fg, bg, true);
|
|
}
|
|
}
|
|
|
|
if (vSelLo < vSelHi) {
|
|
drawTextN(d, ops, font, textX + (vSelLo - scrollCol) * font->charWidth, drawY, expandBuf + vSelLo, vSelHi - vSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
|
|
}
|
|
|
|
if (vSelHi < drawEnd) {
|
|
if (hasSyntax) {
|
|
textEditDrawColorizedText(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, syntaxBuf, vSelHi, fg, bg, custom);
|
|
} else {
|
|
drawTextN(d, ops, font, textX + (vSelHi - scrollCol) * font->charWidth, drawY, expandBuf + vSelHi, drawEnd - vSelHi, fg, bg, true);
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if (drawStart < drawEnd) {
|
|
if (hasSyntax) {
|
|
textEditDrawColorizedText(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, syntaxBuf, drawStart, fg, bg, custom);
|
|
} else {
|
|
drawTextN(d, ops, font, textX + (drawStart - scrollCol) * font->charWidth, drawY, expandBuf + drawStart, drawEnd - drawStart, fg, bg, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
bg = savedBg;
|
|
|
|
lineOff += lineL;
|
|
|
|
if (lineOff < len && buf[lineOff] == '\n') {
|
|
lineOff++;
|
|
}
|
|
}
|
|
|
|
if (showCursor) {
|
|
int32_t curLineOff = textEditLineStart(lc, buf, len, cursorRow);
|
|
int32_t curOff = curLineOff + cursorCol;
|
|
|
|
if (curOff > len) {
|
|
curOff = len;
|
|
}
|
|
|
|
int32_t curVisCol = textEditVisualCol(buf, curLineOff, curOff, tabW);
|
|
int32_t curDrawCol = curVisCol - scrollCol;
|
|
int32_t curDrawRow = cursorRow - 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// This is the core single-line text editing engine, parameterized by
|
|
// pointer to allow reuse across TextInput, Spinner, and ComboBox.
|
|
void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t fieldWidth) {
|
|
bool shift = (mod & KEY_MOD_SHIFT) != 0;
|
|
bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd);
|
|
int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1;
|
|
int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1;
|
|
|
|
// Clamp selection to buffer bounds
|
|
if (hasSel) {
|
|
if (selLo < 0) {
|
|
selLo = 0;
|
|
}
|
|
|
|
if (selHi > *pLen) {
|
|
selHi = *pLen;
|
|
}
|
|
|
|
if (selLo >= selHi) {
|
|
hasSel = false;
|
|
selLo = -1;
|
|
selHi = -1;
|
|
}
|
|
}
|
|
|
|
// Ctrl+A -- select all
|
|
if (key == 1 && pSelStart && pSelEnd) {
|
|
*pSelStart = 0;
|
|
*pSelEnd = *pLen;
|
|
*pCursor = *pLen;
|
|
goto adjustScroll;
|
|
}
|
|
|
|
// Ctrl+C -- copy
|
|
if (key == KEY_CTRL_C) {
|
|
if (hasSel) {
|
|
clipboardCopy(buf + selLo, selHi - selLo);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Ctrl+V -- paste
|
|
if (key == KEY_CTRL_V) {
|
|
int32_t cbLen = 0;
|
|
const char *cb = clipboardGet(&cbLen);
|
|
|
|
if (cbLen > 0) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
if (hasSel) {
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
}
|
|
|
|
int32_t canFit = bufSize - 1 - *pLen;
|
|
// For single-line, skip newlines in clipboard
|
|
int32_t paste = 0;
|
|
|
|
for (int32_t i = 0; i < cbLen && paste < canFit; i++) {
|
|
if (cb[i] != '\n' && cb[i] != '\r') {
|
|
paste++;
|
|
}
|
|
}
|
|
|
|
if (paste > 0) {
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos + paste, buf + pos, *pLen - pos + 1);
|
|
|
|
int32_t j = 0;
|
|
|
|
for (int32_t i = 0; i < cbLen && j < paste; i++) {
|
|
if (cb[i] != '\n' && cb[i] != '\r') {
|
|
buf[pos + j] = cb[i];
|
|
j++;
|
|
}
|
|
}
|
|
|
|
*pLen += paste;
|
|
*pCursor += paste;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
goto adjustScroll;
|
|
}
|
|
|
|
// Ctrl+X -- cut
|
|
if (key == KEY_CTRL_X) {
|
|
if (hasSel) {
|
|
clipboardCopy(buf + selLo, selHi - selLo);
|
|
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
|
|
goto adjustScroll;
|
|
}
|
|
|
|
// Ctrl+Z -- undo
|
|
if (key == KEY_CTRL_Z && undoBuf && pUndoLen && pUndoCursor) {
|
|
// Swap current and undo
|
|
char tmpBuf[*pLen + 1];
|
|
int32_t tmpLen = *pLen;
|
|
int32_t tmpCursor = *pCursor;
|
|
int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1;
|
|
|
|
memcpy(tmpBuf, buf, copyLen);
|
|
tmpBuf[copyLen] = '\0';
|
|
|
|
int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1;
|
|
memcpy(buf, undoBuf, restLen);
|
|
buf[restLen] = '\0';
|
|
*pLen = restLen;
|
|
|
|
int32_t restoreCursor = *pUndoCursor < *pLen ? *pUndoCursor : *pLen;
|
|
|
|
// Save old as new undo
|
|
int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1;
|
|
memcpy(undoBuf, tmpBuf, saveLen);
|
|
undoBuf[saveLen] = '\0';
|
|
*pUndoLen = saveLen;
|
|
*pUndoCursor = tmpCursor;
|
|
|
|
*pCursor = restoreCursor;
|
|
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
goto adjustScroll;
|
|
}
|
|
|
|
if (key >= KEY_ASCII_PRINT_FIRST && key <= KEY_ASCII_PRINT_LAST) {
|
|
// Printable character
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
if (hasSel) {
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
}
|
|
|
|
if (*pLen < bufSize - 1) {
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos + 1, buf + pos, *pLen - pos + 1);
|
|
buf[pos] = (char)key;
|
|
(*pLen)++;
|
|
(*pCursor)++;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else if (key == 8) {
|
|
// Backspace
|
|
if (hasSel) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
} else if (*pCursor > 0) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos - 1, buf + pos, *pLen - pos + 1);
|
|
(*pLen)--;
|
|
(*pCursor)--;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else if (key == KEY_LEFT) {
|
|
// Left arrow
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
}
|
|
|
|
if (*pCursor > 0) {
|
|
(*pCursor)--;
|
|
}
|
|
|
|
*pSelEnd = *pCursor;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
if (*pCursor > 0) {
|
|
(*pCursor)--;
|
|
}
|
|
}
|
|
} else if (key == KEY_RIGHT) {
|
|
// Right arrow
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
}
|
|
|
|
if (*pCursor < *pLen) {
|
|
(*pCursor)++;
|
|
}
|
|
|
|
*pSelEnd = *pCursor;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
if (*pCursor < *pLen) {
|
|
(*pCursor)++;
|
|
}
|
|
}
|
|
} else if (key == (0x73 | 0x100)) {
|
|
// Ctrl+Left -- word left
|
|
int32_t newPos = wordBoundaryLeft(buf, *pCursor);
|
|
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
*pSelEnd = newPos;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
}
|
|
} else if (key == (0x74 | 0x100)) {
|
|
// Ctrl+Right -- word right
|
|
int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor);
|
|
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
*pSelEnd = newPos;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = newPos;
|
|
}
|
|
} else if (key == KEY_HOME) {
|
|
// Home
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
}
|
|
|
|
*pCursor = 0;
|
|
*pSelEnd = 0;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = 0;
|
|
}
|
|
} else if (key == KEY_END) {
|
|
// End
|
|
if (shift && pSelStart && pSelEnd) {
|
|
if (*pSelStart < 0) {
|
|
*pSelStart = *pCursor;
|
|
}
|
|
|
|
*pCursor = *pLen;
|
|
*pSelEnd = *pLen;
|
|
} else {
|
|
if (pSelStart) {
|
|
*pSelStart = -1;
|
|
}
|
|
|
|
if (pSelEnd) {
|
|
*pSelEnd = -1;
|
|
}
|
|
|
|
*pCursor = *pLen;
|
|
}
|
|
} else if (key == KEY_DELETE) {
|
|
// Delete
|
|
if (hasSel) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd);
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
} else if (*pCursor < *pLen) {
|
|
if (undoBuf) {
|
|
textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize);
|
|
}
|
|
|
|
int32_t pos = *pCursor;
|
|
memmove(buf + pos, buf + pos + 1, *pLen - pos);
|
|
(*pLen)--;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
adjustScroll:
|
|
// Adjust scroll offset to keep cursor visible
|
|
{
|
|
AppContextT *ctx = wgtGetContext(w);
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t fieldW = fieldWidth > 0 ? fieldWidth : w->w;
|
|
|
|
int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth;
|
|
|
|
if (*pCursor < *pScrollOff) {
|
|
*pScrollOff = *pCursor;
|
|
}
|
|
|
|
if (*pCursor >= *pScrollOff + visibleChars) {
|
|
*pScrollOff = *pCursor - visibleChars + 1;
|
|
}
|
|
}
|
|
|
|
wgtInvalidatePaint(w);
|
|
}
|
|
|
|
|
|
// Renders a single line of text with optional selection highlighting
|
|
// and a blinking cursor.
|
|
void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX) {
|
|
// Normalize selection to low/high
|
|
int32_t selLo = -1;
|
|
int32_t selHi = -1;
|
|
|
|
if (selStart >= 0 && selEnd >= 0 && selStart != selEnd) {
|
|
selLo = selStart < selEnd ? selStart : selEnd;
|
|
selHi = selStart < selEnd ? selEnd : selStart;
|
|
}
|
|
|
|
// Map selection to visible range
|
|
int32_t visSelLo = selLo - scrollOff;
|
|
int32_t visSelHi = selHi - scrollOff;
|
|
|
|
if (visSelLo < 0) { visSelLo = 0; }
|
|
if (visSelHi > visLen) { visSelHi = visLen; }
|
|
|
|
if (selLo >= 0 && visSelLo < visSelHi) {
|
|
if (visSelLo > 0) {
|
|
drawTextN(d, ops, font, textX, textY, buf, visSelLo, fg, bg, true);
|
|
}
|
|
|
|
drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, buf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true);
|
|
|
|
if (visSelHi < visLen) {
|
|
drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, buf + visSelHi, visLen - visSelHi, fg, bg, true);
|
|
}
|
|
} else if (visLen > 0) {
|
|
drawTextN(d, ops, font, textX, textY, buf, visLen, fg, bg, true);
|
|
}
|
|
|
|
// Blinking cursor
|
|
if (showCursor && sCursorBlinkOn) {
|
|
int32_t cursorX = textX + (cursorPos - scrollOff) * font->charWidth;
|
|
|
|
if (cursorX >= cursorMinX && cursorX < cursorMaxX) {
|
|
drawVLine(d, ops, cursorX, textY, font->charHeight, fg);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Paints a single vertical or horizontal text-grid scrollbar:
|
|
// trough, end-arrow buttons with direction triangles, and thumb.
|
|
// All items are in logical units (lines/cols).
|
|
void widgetTextScrollbarDraw(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, bool vertical, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll) {
|
|
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
|
|
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
|
|
|
if (vertical) {
|
|
drawBevel(d, ops, x, y, thick, len, &troughBevel);
|
|
} else {
|
|
drawBevel(d, ops, x, y, len, thick, &troughBevel);
|
|
}
|
|
|
|
// Starting-end arrow button (up / left)
|
|
drawBevel(d, ops, x, y, thick, thick, &btnBevel);
|
|
|
|
{
|
|
int32_t cx = x + thick / 2;
|
|
int32_t cy = y + thick / 2;
|
|
|
|
if (vertical) {
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawHLine(d, ops, cx - i, cy - 2 + i, 1 + i * 2, colors->contentFg);
|
|
}
|
|
} else {
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, colors->contentFg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ending-end arrow button (down / right)
|
|
int32_t endX = vertical ? x : x + len - thick;
|
|
int32_t endY = vertical ? y + len - thick : y;
|
|
drawBevel(d, ops, endX, endY, thick, thick, &btnBevel);
|
|
|
|
{
|
|
int32_t cx = endX + thick / 2;
|
|
int32_t cy = endY + thick / 2;
|
|
|
|
if (vertical) {
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawHLine(d, ops, cx - i, cy + 2 - i, 1 + i * 2, colors->contentFg);
|
|
}
|
|
} else {
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, colors->contentFg);
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t trackLen = len - thick * 2;
|
|
|
|
if (trackLen > 0) {
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize);
|
|
|
|
if (vertical) {
|
|
drawBevel(d, ops, x, y + thick + thumbPos, thick, thumbSize, &btnBevel);
|
|
} else {
|
|
drawBevel(d, ops, x + thick + thumbPos, y, thumbSize, thick, &btnBevel);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Converts a mouse coordinate (during an active thumb drag) to a
|
|
// clamped scroll value. dragOff is the thumb-relative offset captured
|
|
// when the drag started (from the hit-test's *pDragOff).
|
|
int32_t widgetTextScrollbarDragToScroll(bool vertical, int32_t mouseCoord, int32_t sbStart, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t dragOff) {
|
|
(void)vertical;
|
|
int32_t maxScroll = total - visible;
|
|
|
|
if (maxScroll <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t trackLen = len - thick * 2;
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, total, visible, 0, &thumbPos, &thumbSize);
|
|
|
|
int32_t rel = mouseCoord - sbStart - thick - dragOff;
|
|
int32_t newScroll = (trackLen > thumbSize) ? (maxScroll * rel) / (trackLen - thumbSize) : 0;
|
|
|
|
if (newScroll < 0) {
|
|
newScroll = 0;
|
|
}
|
|
|
|
if (newScroll > maxScroll) {
|
|
newScroll = maxScroll;
|
|
}
|
|
|
|
return newScroll;
|
|
}
|
|
|
|
|
|
// Classifies a mouse click against a text-grid scrollbar. Returns
|
|
// one of the TEXT_SB_HIT_* codes. Callers typically translate UP/
|
|
// DOWN to single-step scrolls and PAGE_UP/PAGE_DOWN to page-sized
|
|
// jumps. THUMB begins a drag; *pDragOff captures the click offset
|
|
// relative to the thumb origin for use in subsequent drag updates.
|
|
int32_t widgetTextScrollbarHitTest(bool vertical, int32_t vx, int32_t vy, int32_t x, int32_t y, int32_t thick, int32_t len, int32_t total, int32_t visible, int32_t scroll, int32_t *pDragOff) {
|
|
int32_t primary = vertical ? (vy - y) : (vx - x);
|
|
int32_t cross = vertical ? (vx - x) : (vy - y);
|
|
|
|
if (cross < 0 || cross >= thick || primary < 0 || primary >= len) {
|
|
return TEXT_SB_HIT_NONE;
|
|
}
|
|
|
|
if (primary < thick) {
|
|
return TEXT_SB_HIT_UP;
|
|
}
|
|
|
|
if (primary >= len - thick) {
|
|
return TEXT_SB_HIT_DOWN;
|
|
}
|
|
|
|
int32_t trackLen = len - thick * 2;
|
|
|
|
if (trackLen <= 0) {
|
|
return TEXT_SB_HIT_NONE;
|
|
}
|
|
|
|
int32_t thumbPos;
|
|
int32_t thumbSize;
|
|
widgetScrollbarThumb(trackLen, total, visible, scroll, &thumbPos, &thumbSize);
|
|
|
|
int32_t trackRel = primary - thick;
|
|
|
|
if (trackRel < thumbPos) {
|
|
return TEXT_SB_HIT_PAGE_UP;
|
|
}
|
|
|
|
if (trackRel >= thumbPos + thumbSize) {
|
|
return TEXT_SB_HIT_PAGE_DOWN;
|
|
}
|
|
|
|
if (pDragOff) {
|
|
*pDragOff = trackRel - thumbPos;
|
|
}
|
|
|
|
return TEXT_SB_HIT_THUMB;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) {
|
|
if (pos >= len) {
|
|
return len;
|
|
}
|
|
|
|
// Skip word characters
|
|
while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) {
|
|
pos++;
|
|
}
|
|
|
|
// Skip non-word characters
|
|
while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') {
|
|
pos++;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
int32_t wordEnd(const char *buf, int32_t len, int32_t pos) {
|
|
while (pos < len && isWordChar(buf[pos])) {
|
|
pos++;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
int32_t wordStart(const char *buf, int32_t pos) {
|
|
while (pos > 0 && isWordChar(buf[pos - 1])) {
|
|
pos--;
|
|
}
|
|
|
|
return pos;
|
|
}
|