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