From fd41390085d565d60875ed5f9d80f4c8f2a08c75 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Sat, 14 Mar 2026 17:50:38 -0500 Subject: [PATCH] Much improved text editing. More widget demos. --- .gitattributes | 2 + dvx/dvxApp.c | 8 +- dvx/dvxWidget.h | 23 + dvx/widgets/widgetAnsiTerm.c | 318 ++++- dvx/widgets/widgetButton.c | 4 +- dvx/widgets/widgetCheckbox.c | 4 +- dvx/widgets/widgetClass.c | 14 +- dvx/widgets/widgetComboBox.c | 137 ++- dvx/widgets/widgetCore.c | 5 +- dvx/widgets/widgetDropdown.c | 4 +- dvx/widgets/widgetEvent.c | 20 +- dvx/widgets/widgetImageButton.c | 4 +- dvx/widgets/widgetInternal.h | 48 +- dvx/widgets/widgetListBox.c | 4 +- dvx/widgets/widgetRadio.c | 4 +- dvx/widgets/widgetSlider.c | 4 +- dvx/widgets/widgetTabControl.c | 4 +- dvx/widgets/widgetTextInput.c | 2000 +++++++++++++++++++++++++++++-- dvx/widgets/widgetTreeView.c | 4 +- dvxdemo/demo.c | 150 ++- dvxdemo/logo.bmp | 3 + dvxdemo/new.bmp | 3 + dvxdemo/open.bmp | 3 + dvxdemo/sample.bmp | 3 + dvxdemo/save.bmp | 3 + 25 files changed, 2558 insertions(+), 218 deletions(-) create mode 100644 .gitattributes create mode 100644 dvxdemo/logo.bmp create mode 100644 dvxdemo/new.bmp create mode 100644 dvxdemo/open.bmp create mode 100644 dvxdemo/sample.bmp create mode 100644 dvxdemo/save.bmp diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1d0c89a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.bmp filter=lfs diff=lfs merge=lfs -text +*.BMP filter=lfs diff=lfs merge=lfs -text diff --git a/dvx/dvxApp.c b/dvx/dvxApp.c index ef21b2d..05a5aab 100644 --- a/dvx/dvxApp.c +++ b/dvx/dvxApp.c @@ -10,6 +10,7 @@ #include #include #include +#include #define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2) #define ICON_REFRESH_INTERVAL 8 @@ -741,6 +742,9 @@ const BitmapFontT *dvxGetFont(const AppContextT *ctx) { int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) { memset(ctx, 0, sizeof(*ctx)); + // Disable Ctrl+C/Break termination + signal(SIGINT, SIG_IGN); + // Initialize video if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { return -1; @@ -1935,7 +1939,7 @@ static void pollKeyboard(AppContextT *ctx) { if (termFocused) { // Terminal has focus — send Tab to it if (win->onKey) { - win->onKey(win, ascii ? ascii : (scancode | 0x100), 0); + win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags); } continue; @@ -1994,7 +1998,7 @@ static void pollKeyboard(AppContextT *ctx) { WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; if (win->onKey) { - win->onKey(win, ascii ? ascii : (scancode | 0x100), 0); + win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags); } } diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index e9425a5..a52f1d0 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -182,6 +182,11 @@ typedef struct WidgetT { int32_t len; int32_t cursorPos; int32_t scrollOff; + int32_t selStart; // selection anchor (-1 = none) + int32_t selEnd; // selection end (-1 = none) + char *undoBuf; + int32_t undoLen; + int32_t undoCursor; } textInput; struct { @@ -191,6 +196,13 @@ typedef struct WidgetT { int32_t cursorRow; int32_t cursorCol; int32_t scrollRow; + int32_t scrollCol; + int32_t desiredCol; // sticky column for up/down movement + int32_t selAnchor; // selection anchor byte offset (-1 = none) + int32_t selCursor; // selection cursor byte offset (-1 = none) + char *undoBuf; + int32_t undoLen; + int32_t undoCursor; // byte offset at time of snapshot } textArea; struct { @@ -225,6 +237,11 @@ typedef struct WidgetT { int32_t len; int32_t cursorPos; int32_t scrollOff; + int32_t selStart; // selection anchor (-1 = none) + int32_t selEnd; // selection end (-1 = none) + char *undoBuf; + int32_t undoLen; + int32_t undoCursor; const char **items; int32_t itemCount; int32_t selectedIdx; @@ -330,6 +347,12 @@ typedef struct WidgetT { // Cached packed palette (avoids packColor per repaint) uint32_t packedPalette[16]; bool paletteValid; + // Selection (line indices in scrollback+screen space) + int32_t selStartLine; + int32_t selStartCol; + int32_t selEndLine; + int32_t selEndCol; + bool selecting; // Communications interface (all NULL = disconnected) void *commCtx; int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); diff --git a/dvx/widgets/widgetAnsiTerm.c b/dvx/widgets/widgetAnsiTerm.c index 4f0f060..7ea9fe5 100644 --- a/dvx/widgets/widgetAnsiTerm.c +++ b/dvx/widgets/widgetAnsiTerm.c @@ -61,6 +61,8 @@ static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 }; static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow); static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d); +static void ansiTermClearSelection(WidgetT *w); +static void ansiTermCopySelection(WidgetT *w); static void ansiTermDeleteLines(WidgetT *w, int32_t count); static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count); static void ansiTermDirtyRow(WidgetT *w, int32_t row); @@ -69,13 +71,17 @@ static void ansiTermEraseDisplay(WidgetT *w, int32_t mode); static void ansiTermEraseLine(WidgetT *w, int32_t mode); static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count); static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex); +static bool ansiTermHasSelection(const WidgetT *w); static void ansiTermInsertLines(WidgetT *w, int32_t count); static void ansiTermNewline(WidgetT *w); +static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY); +static void ansiTermPasteToComm(WidgetT *w); static void ansiTermProcessByte(WidgetT *w, uint8_t ch); static void ansiTermProcessSgr(WidgetT *w); static void ansiTermPutChar(WidgetT *w, uint8_t ch); static void ansiTermScrollDown(WidgetT *w); static void ansiTermScrollUp(WidgetT *w); +static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol); // ============================================================ @@ -126,6 +132,77 @@ static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) { } +// ============================================================ +// ansiTermClearSelection +// ============================================================ + +static void ansiTermClearSelection(WidgetT *w) { + if (ansiTermHasSelection(w)) { + w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; + } + + w->as.ansiTerm.selStartLine = -1; + w->as.ansiTerm.selStartCol = -1; + w->as.ansiTerm.selEndLine = -1; + w->as.ansiTerm.selEndCol = -1; + w->as.ansiTerm.selecting = false; +} + + +// ============================================================ +// ansiTermCopySelection +// ============================================================ + +static void ansiTermCopySelection(WidgetT *w) { + if (!ansiTermHasSelection(w)) { + return; + } + + int32_t sLine; + int32_t sCol; + int32_t eLine; + int32_t eCol; + ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); + + int32_t cols = w->as.ansiTerm.cols; + + // Build text from selected cells (strip trailing spaces per line) + char buf[4096]; + int32_t pos = 0; + + for (int32_t line = sLine; line <= eLine && pos < 4095; line++) { + const uint8_t *lineData = ansiTermGetLine(w, line); + + int32_t colStart = (line == sLine) ? sCol : 0; + int32_t colEnd = (line == eLine) ? eCol : cols; + + // Find last non-space character in this line's selection + int32_t lastNonSpace = colStart - 1; + + for (int32_t c = colStart; c < colEnd; c++) { + if (lineData[c * 2] != ' ') { + lastNonSpace = c; + } + } + + for (int32_t c = colStart; c <= lastNonSpace && pos < 4095; c++) { + buf[pos++] = (char)lineData[c * 2]; + } + + // Add newline between lines (not after last) + if (line < eLine && pos < 4095) { + buf[pos++] = '\n'; + } + } + + buf[pos] = '\0'; + + if (pos > 0) { + clipboardCopy(buf, pos); + } +} + + // ============================================================ // ansiTermDirtyRange // ============================================================ @@ -643,6 +720,24 @@ static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { } +// ============================================================ +// ansiTermHasSelection +// ============================================================ + +static bool ansiTermHasSelection(const WidgetT *w) { + if (w->as.ansiTerm.selStartLine < 0) { + return false; + } + + if (w->as.ansiTerm.selStartLine == w->as.ansiTerm.selEndLine && + w->as.ansiTerm.selStartCol == w->as.ansiTerm.selEndCol) { + return false; + } + + return true; +} + + // ============================================================ // ansiTermInsertLines // ============================================================ @@ -866,6 +961,35 @@ static void ansiTermProcessSgr(WidgetT *w) { } +// ============================================================ +// ansiTermPasteToComm +// ============================================================ + +static void ansiTermPasteToComm(WidgetT *w) { + if (!w->as.ansiTerm.commWrite) { + return; + } + + int32_t clipLen; + const char *clip = clipboardGet(&clipLen); + + if (clipLen <= 0) { + return; + } + + // Transmit clipboard contents, converting \n to \r for the terminal + for (int32_t i = 0; i < clipLen; i++) { + uint8_t ch = (uint8_t)clip[i]; + + if (ch == '\n') { + ch = '\r'; + } + + w->as.ansiTerm.commWrite(w->as.ansiTerm.commCtx, &ch, 1); + } +} + + // ============================================================ // ansiTermPutChar // ============================================================ @@ -967,6 +1091,32 @@ static void ansiTermScrollUp(WidgetT *w) { } +// ============================================================ +// ansiTermSelectionRange +// ============================================================ +// +// Return selection start/end in normalized order (start <= end). + +static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol) { + int32_t sl = w->as.ansiTerm.selStartLine; + int32_t sc = w->as.ansiTerm.selStartCol; + int32_t el = w->as.ansiTerm.selEndLine; + int32_t ec = w->as.ansiTerm.selEndCol; + + if (sl > el || (sl == el && sc > ec)) { + *startLine = el; + *startCol = ec; + *endLine = sl; + *endCol = sc; + } else { + *startLine = sl; + *startCol = sc; + *endLine = el; + *endCol = ec; + } +} + + // ============================================================ // wgtAnsiTerm // ============================================================ @@ -1031,6 +1181,11 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { w->as.ansiTerm.commCtx = NULL; w->as.ansiTerm.commRead = NULL; w->as.ansiTerm.commWrite = NULL; + w->as.ansiTerm.selStartLine = -1; + w->as.ansiTerm.selStartCol = -1; + w->as.ansiTerm.selEndLine = -1; + w->as.ansiTerm.selEndCol = -1; + w->as.ansiTerm.selecting = false; w->as.ansiTerm.blinkVisible = true; w->as.ansiTerm.blinkTime = clock(); w->as.ansiTerm.cursorOn = true; @@ -1267,6 +1422,7 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { } drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol2); + ansiTermPaintSelRow(w, &cd, ops, font, row, baseX, baseY); if (row < minRow) { minRow = row; } if (row > maxRow) { maxRow = row; } @@ -1308,6 +1464,8 @@ void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { return; } + ansiTermClearSelection(w); + for (int32_t i = 0; i < len; i++) { ansiTermProcessByte(w, data[i]); } @@ -1334,6 +1492,54 @@ void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { } +// ============================================================ +// ansiTermPaintSelRow +// ============================================================ +// +// Overlay selection highlighting for a single terminal row. +// Inverts fg/bg for selected cells. + +static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY) { + if (!ansiTermHasSelection(w)) { + return; + } + + int32_t sLine; + int32_t sCol; + int32_t eLine; + int32_t eCol; + ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); + + int32_t cols = w->as.ansiTerm.cols; + int32_t lineIndex = w->as.ansiTerm.scrollPos + screenRow; + + if (lineIndex < sLine || lineIndex > eLine) { + return; + } + + int32_t colStart = (lineIndex == sLine) ? sCol : 0; + int32_t colEnd = (lineIndex == eLine) ? eCol : cols; + + if (colStart >= colEnd) { + return; + } + + const uint8_t *lineData = ansiTermGetLine(w, lineIndex); + const uint32_t *palette = w->as.ansiTerm.packedPalette; + int32_t cellH = font->charHeight; + int32_t cellW = font->charWidth; + + for (int32_t col = colStart; col < colEnd; col++) { + uint8_t ch = lineData[col * 2]; + uint8_t attr = lineData[col * 2 + 1]; + uint32_t fg = palette[(attr >> 4) & 0x07]; // swap: bg becomes fg + uint32_t bg = palette[attr & 0x0F]; // swap: fg becomes bg + + drawChar(d, ops, font, baseX + col * cellW, baseY + screenRow * cellH, ch, fg, bg, true); + } +} + + // ============================================================ // widgetAnsiTermOnKey // ============================================================ @@ -1341,11 +1547,35 @@ void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { // Translate keyboard input to ANSI escape sequences and send // via the comm interface. Does nothing if commWrite is NULL. -void widgetAnsiTermOnKey(WidgetT *w, int32_t key) { +void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { + // Ctrl+C: copy if selection exists, otherwise send ^C + if (key == 0x03 && (mod & KEY_MOD_CTRL)) { + if (ansiTermHasSelection(w)) { + ansiTermCopySelection(w); + ansiTermClearSelection(w); + wgtInvalidate(w); + return; + } + + // No selection — fall through to send ^C to terminal + } + + // Ctrl+V: paste from clipboard to terminal + if (key == 0x16 && (mod & KEY_MOD_CTRL)) { + ansiTermPasteToComm(w); + wgtInvalidate(w); + return; + } + if (!w->as.ansiTerm.commWrite) { return; } + // Any keypress clears selection + if (ansiTermHasSelection(w)) { + ansiTermClearSelection(w); + } + uint8_t buf[8]; int32_t len = 0; @@ -1405,6 +1635,10 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key) { // Delete → ESC[3~ buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; len = 4; + } else if (key >= 1 && key < 32) { + // Control characters (^A=1, ^B=2, ^C=3, etc.) + buf[0] = (uint8_t)key; + len = 1; } if (len > 0) { @@ -1425,14 +1659,8 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) AppContextT *actx = (AppContextT *)root->userData; const BitmapFontT *font = &actx->font; hit->focused = true; + clearOtherSelections(hit); - int32_t sbCount = hit->as.ansiTerm.scrollbackCount; - - if (sbCount == 0) { - return; - } - - // Scrollbar geometry int32_t cols = hit->as.ansiTerm.cols; int32_t rows = hit->as.ansiTerm.rows; int32_t sbX = hit->x + ANSI_BORDER + cols * font->charWidth; @@ -1440,7 +1668,78 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) int32_t sbH = rows * font->charHeight; int32_t arrowH = ANSI_SB_W; - if (vx < sbX || vx >= sbX + ANSI_SB_W) { + // Click in text area — start selection + if (vx < sbX) { + int32_t baseX = hit->x + ANSI_BORDER; + int32_t baseY = hit->y + ANSI_BORDER; + int32_t clickRow = (vy - baseY) / font->charHeight; + int32_t clickCol = (vx - baseX) / font->charWidth; + + if (clickRow < 0) { + clickRow = 0; + } + + if (clickRow >= rows) { + clickRow = rows - 1; + } + + if (clickCol < 0) { + clickCol = 0; + } + + if (clickCol >= cols) { + clickCol = cols - 1; + } + + int32_t lineIndex = hit->as.ansiTerm.scrollPos + clickRow; + + int32_t clicks = multiClickDetect(vx, vy); + + if (clicks >= 3) { + // Triple-click: select entire line + hit->as.ansiTerm.selStartLine = lineIndex; + hit->as.ansiTerm.selStartCol = 0; + hit->as.ansiTerm.selEndLine = lineIndex; + hit->as.ansiTerm.selEndCol = cols; + hit->as.ansiTerm.selecting = false; + sDragTextSelect = NULL; + } else if (clicks == 2) { + // Double-click: select word + const uint8_t *lineData = ansiTermGetLine(hit, lineIndex); + int32_t ws = clickCol; + int32_t we = clickCol; + + while (ws > 0 && isWordChar((char)lineData[(ws - 1) * 2])) { + ws--; + } + + while (we < cols && isWordChar((char)lineData[we * 2])) { + we++; + } + + hit->as.ansiTerm.selStartLine = lineIndex; + hit->as.ansiTerm.selStartCol = ws; + hit->as.ansiTerm.selEndLine = lineIndex; + hit->as.ansiTerm.selEndCol = we; + hit->as.ansiTerm.selecting = false; + sDragTextSelect = NULL; + } else { + // Single click: start selection anchor + hit->as.ansiTerm.selStartLine = lineIndex; + hit->as.ansiTerm.selStartCol = clickCol; + hit->as.ansiTerm.selEndLine = lineIndex; + hit->as.ansiTerm.selEndCol = clickCol; + hit->as.ansiTerm.selecting = true; + sDragTextSelect = hit; + } + + hit->as.ansiTerm.dirtyRows = 0xFFFFFFFF; + return; + } + + int32_t sbCount = hit->as.ansiTerm.scrollbackCount; + + if (sbCount == 0) { return; } @@ -1536,6 +1835,7 @@ void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm.blinkVisible, curCol); + ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY); } // Draw scrollbar diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c index 6c0108d..b6c18f5 100644 --- a/dvx/widgets/widgetButton.c +++ b/dvx/widgets/widgetButton.c @@ -53,7 +53,9 @@ void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetButtonOnKey // ============================================================ -void widgetButtonOnKey(WidgetT *w, int32_t key) { +void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + if (key == ' ' || key == 0x0D) { w->as.button.pressed = true; sKeyPressedBtn = w; diff --git a/dvx/widgets/widgetCheckbox.c b/dvx/widgets/widgetCheckbox.c index ef2c1c6..9f20d3f 100644 --- a/dvx/widgets/widgetCheckbox.c +++ b/dvx/widgets/widgetCheckbox.c @@ -54,7 +54,9 @@ void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetCheckboxOnKey // ============================================================ -void widgetCheckboxOnKey(WidgetT *w, int32_t key) { +void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + if (key == ' ' || key == 0x0D) { w->as.checkbox.checked = !w->as.checkbox.checked; diff --git a/dvx/widgets/widgetClass.c b/dvx/widgets/widgetClass.c index 12a2219..b510d4a 100644 --- a/dvx/widgets/widgetClass.c +++ b/dvx/widgets/widgetClass.c @@ -111,16 +111,16 @@ static const WidgetClassT sClassTextInput = { }; static const WidgetClassT sClassTextArea = { - .flags = 0, - .paint = NULL, + .flags = WCLASS_FOCUSABLE, + .paint = widgetTextAreaPaint, .paintOverlay = NULL, - .calcMinSize = NULL, + .calcMinSize = widgetTextAreaCalcMinSize, .layout = NULL, - .onMouse = NULL, - .onKey = NULL, + .onMouse = widgetTextAreaOnMouse, + .onKey = widgetTextAreaOnKey, .destroy = widgetTextAreaDestroy, - .getText = NULL, - .setText = NULL + .getText = widgetTextAreaGetText, + .setText = widgetTextAreaSetText }; static const WidgetClassT sClassListBox = { diff --git a/dvx/widgets/widgetComboBox.c b/dvx/widgets/widgetComboBox.c index 0586013..361ff65 100644 --- a/dvx/widgets/widgetComboBox.c +++ b/dvx/widgets/widgetComboBox.c @@ -19,6 +19,9 @@ WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) { w->as.comboBox.buf[0] = '\0'; } + w->as.comboBox.undoBuf = (char *)malloc(bufSize); + w->as.comboBox.selStart = -1; + w->as.comboBox.selEnd = -1; w->as.comboBox.selectedIdx = -1; w->weight = 100; } @@ -76,39 +79,8 @@ void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) { w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf); w->as.comboBox.cursorPos = w->as.comboBox.len; w->as.comboBox.scrollOff = 0; - } -} - - -// ============================================================ -// widgetComboBoxDestroy -// ============================================================ - -void widgetComboBoxDestroy(WidgetT *w) { - free(w->as.comboBox.buf); -} - - -// ============================================================ -// widgetComboBoxGetText -// ============================================================ - -const char *widgetComboBoxGetText(const WidgetT *w) { - return w->as.comboBox.buf ? w->as.comboBox.buf : ""; -} - - -// ============================================================ -// widgetComboBoxSetText -// ============================================================ - -void widgetComboBoxSetText(WidgetT *w, const char *text) { - if (w->as.comboBox.buf) { - strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1); - w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0'; - w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf); - w->as.comboBox.cursorPos = w->as.comboBox.len; - w->as.comboBox.scrollOff = 0; + w->as.comboBox.selStart = -1; + w->as.comboBox.selEnd = -1; } } @@ -133,11 +105,30 @@ void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { } +// ============================================================ +// widgetComboBoxDestroy +// ============================================================ + +void widgetComboBoxDestroy(WidgetT *w) { + free(w->as.comboBox.buf); + free(w->as.comboBox.undoBuf); +} + + +// ============================================================ +// widgetComboBoxGetText +// ============================================================ + +const char *widgetComboBoxGetText(const WidgetT *w) { + return w->as.comboBox.buf ? w->as.comboBox.buf : ""; +} + + // ============================================================ // widgetComboBoxOnKey // ============================================================ -void widgetComboBoxOnKey(WidgetT *w, int32_t key) { +void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { if (w->as.comboBox.open) { if (key == (0x48 | 0x100)) { if (w->as.comboBox.hoverIdx > 0) { @@ -177,6 +168,8 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key) { w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf); w->as.comboBox.cursorPos = w->as.comboBox.len; w->as.comboBox.scrollOff = 0; + w->as.comboBox.selStart = -1; + w->as.comboBox.selEnd = -1; } w->as.comboBox.open = false; @@ -214,9 +207,14 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key) { return; } - widgetTextEditOnKey(w, key, w->as.comboBox.buf, w->as.comboBox.bufSize, + clearOtherSelections(w); + + widgetTextEditOnKey(w, key, mod, w->as.comboBox.buf, w->as.comboBox.bufSize, &w->as.comboBox.len, &w->as.comboBox.cursorPos, - &w->as.comboBox.scrollOff); + &w->as.comboBox.scrollOff, + &w->as.comboBox.selStart, &w->as.comboBox.selEnd, + w->as.comboBox.undoBuf, &w->as.comboBox.undoLen, + &w->as.comboBox.undoCursor); } @@ -225,7 +223,6 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key) { // ============================================================ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { - (void)vy; w->focused = true; // Check if click is on the button area @@ -243,6 +240,8 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { sOpenPopup = w->as.comboBox.open ? w : NULL; } else { // Text area click — focus for editing + clearOtherSelections(w); + AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t relX = vx - w->x - TEXT_INPUT_PAD; @@ -256,7 +255,34 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { charPos = w->as.comboBox.len; } + int32_t clicks = multiClickDetect(vx, vy); + + if (clicks >= 3) { + // Triple-click: select all (single line) + w->as.comboBox.selStart = 0; + w->as.comboBox.selEnd = w->as.comboBox.len; + w->as.comboBox.cursorPos = w->as.comboBox.len; + sDragTextSelect = NULL; + return; + } + + if (clicks == 2 && w->as.comboBox.buf) { + // Double-click: select word + int32_t ws = wordStart(w->as.comboBox.buf, charPos); + int32_t we = wordEnd(w->as.comboBox.buf, w->as.comboBox.len, charPos); + + w->as.comboBox.selStart = ws; + w->as.comboBox.selEnd = we; + w->as.comboBox.cursorPos = we; + sDragTextSelect = NULL; + return; + } + + // Single click: place cursor + start drag-select w->as.comboBox.cursorPos = charPos; + w->as.comboBox.selStart = charPos; + w->as.comboBox.selEnd = charPos; + sDragTextSelect = w; } } @@ -290,9 +316,27 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit len = maxChars; } + // Selection range + int32_t selLo = -1; + int32_t selHi = -1; + + if (w->as.comboBox.selStart >= 0 && w->as.comboBox.selEnd >= 0 && w->as.comboBox.selStart != w->as.comboBox.selEnd) { + selLo = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selStart : w->as.comboBox.selEnd; + selHi = w->as.comboBox.selStart < w->as.comboBox.selEnd ? w->as.comboBox.selEnd : w->as.comboBox.selStart; + } + for (int32_t i = 0; i < len; i++) { + int32_t charIdx = off + i; + uint32_t cfgc = fg; + uint32_t cbgc = bg; + + if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) { + cfgc = colors->menuHighlightFg; + cbgc = colors->menuHighlightBg; + } + drawChar(d, ops, font, textX + i * font->charWidth, textY, - w->as.comboBox.buf[off + i], fg, bg, true); + w->as.comboBox.buf[charIdx], cfgc, cbgc, true); } // Draw cursor @@ -370,3 +414,20 @@ void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, idx == hoverIdx); } } + + +// ============================================================ +// widgetComboBoxSetText +// ============================================================ + +void widgetComboBoxSetText(WidgetT *w, const char *text) { + if (w->as.comboBox.buf) { + strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1); + w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0'; + w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf); + w->as.comboBox.cursorPos = w->as.comboBox.len; + w->as.comboBox.scrollOff = 0; + w->as.comboBox.selStart = -1; + w->as.comboBox.selEnd = -1; + } +} diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index b621776..8ba7ef6 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -10,8 +10,9 @@ bool sDebugLayout = false; WidgetT *sOpenPopup = NULL; WidgetT *sPressedButton = NULL; WidgetT *sDragSlider = NULL; -WidgetT *sDrawingCanvas = NULL; -int32_t sDragOffset = 0; +WidgetT *sDrawingCanvas = NULL; +WidgetT *sDragTextSelect = NULL; +int32_t sDragOffset = 0; // ============================================================ diff --git a/dvx/widgets/widgetDropdown.c b/dvx/widgets/widgetDropdown.c index 61cc8a1..0dcdb4e 100644 --- a/dvx/widgets/widgetDropdown.c +++ b/dvx/widgets/widgetDropdown.c @@ -101,7 +101,9 @@ void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetDropdownOnKey // ============================================================ -void widgetDropdownOnKey(WidgetT *w, int32_t key) { +void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + if (w->as.dropdown.open) { // Popup is open — navigate items if (key == (0x48 | 0x100)) { diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 5fb0c21..68030be 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -108,7 +108,6 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) { // ============================================================ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { - (void)mod; WidgetT *root = win->widgetRoot; if (!root) { @@ -143,7 +142,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { // Dispatch to per-widget onKey handler via vtable if (focus->wclass && focus->wclass->onKey) { - focus->wclass->onKey(focus, key); + focus->wclass->onKey(focus, key, mod); } } @@ -171,6 +170,23 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { sOpenPopup = NULL; } + // Handle text drag-select release + if (sDragTextSelect && !(buttons & 1)) { + sDragTextSelect = NULL; + return; + } + + // Handle text drag-select (mouse move while pressed) + if (sDragTextSelect && (buttons & 1)) { + int32_t scrollX = win->hScroll ? win->hScroll->value : 0; + int32_t scrollY = win->vScroll ? win->vScroll->value : 0; + int32_t vx = x + scrollX; + int32_t vy = y + scrollY; + widgetTextDragUpdate(sDragTextSelect, root, vx, vy); + wgtInvalidate(root); + return; + } + // Handle canvas drawing release if (sDrawingCanvas && !(buttons & 1)) { sDrawingCanvas->as.canvas.lastX = -1; diff --git a/dvx/widgets/widgetImageButton.c b/dvx/widgets/widgetImageButton.c index 2b8ffd2..2452b45 100644 --- a/dvx/widgets/widgetImageButton.c +++ b/dvx/widgets/widgetImageButton.c @@ -71,7 +71,9 @@ void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetImageButtonOnKey // ============================================================ -void widgetImageButtonOnKey(WidgetT *w, int32_t key) { +void widgetImageButtonOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + if (key == ' ' || key == 0x0D) { w->as.imageButton.pressed = true; sKeyPressedBtn = w; diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 703aee9..ba90575 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -29,7 +29,7 @@ typedef struct WidgetClassT { void (*calcMinSize)(WidgetT *w, const BitmapFontT *font); void (*layout)(WidgetT *w, const BitmapFontT *font); void (*onMouse)(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); - void (*onKey)(WidgetT *w, int32_t key); + void (*onKey)(WidgetT *w, int32_t key, int32_t mod); void (*destroy)(WidgetT *w); const char *(*getText)(const WidgetT *w); void (*setText)(WidgetT *w, const char *text); @@ -41,6 +41,11 @@ extern const WidgetClassT *widgetClassTable[]; // Constants // ============================================================ +// Modifier flags (BIOS INT 16h shift state bits) +#define KEY_MOD_SHIFT 0x03 +#define KEY_MOD_CTRL 0x04 +#define KEY_MOD_ALT 0x08 + #define DEFAULT_SPACING 4 #define DEFAULT_PADDING 4 #define SEPARATOR_THICKNESS 2 @@ -87,6 +92,7 @@ extern WidgetT *sOpenPopup; extern WidgetT *sPressedButton; extern WidgetT *sDragSlider; extern WidgetT *sDrawingCanvas; +extern WidgetT *sDragTextSelect; extern int32_t sDragOffset; // ============================================================ @@ -160,6 +166,7 @@ void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); @@ -184,6 +191,7 @@ void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font); +void widgetTextAreaCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font); @@ -209,6 +217,8 @@ const char *widgetLabelGetText(const WidgetT *w); void widgetLabelSetText(WidgetT *w, const char *text); const char *widgetRadioGetText(const WidgetT *w); void widgetRadioSetText(WidgetT *w, const char *text); +const char *widgetTextAreaGetText(const WidgetT *w); +void widgetTextAreaSetText(WidgetT *w, const char *text); const char *widgetTextInputGetText(const WidgetT *w); void widgetTextInputSetText(WidgetT *w, const char *text); const char *widgetTreeItemGetText(const WidgetT *w); @@ -230,32 +240,42 @@ void widgetTextInputDestroy(WidgetT *w); // Per-widget mouse/key functions // ============================================================ -void widgetAnsiTermOnKey(WidgetT *w, int32_t key); +void clearOtherSelections(WidgetT *except); +void clipboardCopy(const char *text, int32_t len); +const char *clipboardGet(int32_t *outLen); +bool isWordChar(char c); +int32_t multiClickDetect(int32_t vx, int32_t vy); +void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetAnsiTermOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetButtonOnKey(WidgetT *w, int32_t key); +void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetCanvasOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetCheckboxOnKey(WidgetT *w, int32_t key); +void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetComboBoxOnKey(WidgetT *w, int32_t key); +void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetDropdownOnKey(WidgetT *w, int32_t key); +void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetImageButtonOnKey(WidgetT *w, int32_t key); +void widgetImageButtonOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetListBoxOnKey(WidgetT *w, int32_t key); +void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetListBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetRadioOnKey(WidgetT *w, int32_t key); +void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetSliderOnKey(WidgetT *w, int32_t key); +void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetTabControlOnKey(WidgetT *w, int32_t key); +void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetTabControlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetTextEditOnKey(WidgetT *w, int32_t key, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff); -void widgetTextInputOnKey(WidgetT *w, int32_t key); +void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod); +void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); +void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); +void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); +void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetTreeViewOnKey(WidgetT *w, int32_t key); +int32_t wordEnd(const char *buf, int32_t len, int32_t pos); +int32_t wordStart(const char *buf, int32_t pos); +void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod); void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); #endif // WIDGET_INTERNAL_H diff --git a/dvx/widgets/widgetListBox.c b/dvx/widgets/widgetListBox.c index 7db1b90..6d662ad 100644 --- a/dvx/widgets/widgetListBox.c +++ b/dvx/widgets/widgetListBox.c @@ -95,7 +95,9 @@ void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetListBoxOnKey // ============================================================ -void widgetListBoxOnKey(WidgetT *w, int32_t key) { +void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) { return; } diff --git a/dvx/widgets/widgetRadio.c b/dvx/widgets/widgetRadio.c index ae3333f..1d125f6 100644 --- a/dvx/widgets/widgetRadio.c +++ b/dvx/widgets/widgetRadio.c @@ -79,7 +79,9 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetRadioOnKey // ============================================================ -void widgetRadioOnKey(WidgetT *w, int32_t key) { +void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + if (key == ' ' || key == 0x0D) { // Select this radio if (w->parent && w->parent->type == WidgetRadioGroupE) { diff --git a/dvx/widgets/widgetSlider.c b/dvx/widgets/widgetSlider.c index 4221bb3..ff6c95b 100644 --- a/dvx/widgets/widgetSlider.c +++ b/dvx/widgets/widgetSlider.c @@ -77,7 +77,9 @@ void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetSliderOnKey // ============================================================ -void widgetSliderOnKey(WidgetT *w, int32_t key) { +void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + int32_t step = 1; int32_t range = w->as.slider.maxValue - w->as.slider.minValue; diff --git a/dvx/widgets/widgetTabControl.c b/dvx/widgets/widgetTabControl.c index 3e38cb2..8d66129 100644 --- a/dvx/widgets/widgetTabControl.c +++ b/dvx/widgets/widgetTabControl.c @@ -131,7 +131,9 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) { // widgetTabControlOnKey // ============================================================ -void widgetTabControlOnKey(WidgetT *w, int32_t key) { +void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + int32_t tabCount = 0; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { diff --git a/dvx/widgets/widgetTextInput.c b/dvx/widgets/widgetTextInput.c index e23df2b..bdce545 100644 --- a/dvx/widgets/widgetTextInput.c +++ b/dvx/widgets/widgetTextInput.c @@ -2,6 +2,247 @@ #include "widgetInternal.h" +#include +#include + +#define TEXTAREA_BORDER 2 +#define TEXTAREA_PAD 2 +#define TEXTAREA_SB_W 14 +#define TEXTAREA_MIN_ROWS 4 +#define TEXTAREA_MIN_COLS 20 +#define CLIPBOARD_MAX 4096 +#define DBLCLICK_TICKS (CLOCKS_PER_SEC / 2) + +// ============================================================ +// Prototypes +// ============================================================ + +static int32_t textAreaCountLines(const char *buf, int32_t len); +static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col); +static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols); +static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row); +static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row); +static int32_t textAreaMaxLineLen(const char *buf, int32_t len); +static void textAreaOffToRowCol(const char *buf, int32_t off, int32_t *row, int32_t *col); +static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); +static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); + +// ============================================================ +// Shared clipboard +// ============================================================ + +static char sClipboard[CLIPBOARD_MAX]; +static int32_t sClipboardLen = 0; + + +void clipboardCopy(const char *text, int32_t len) { + if (len > CLIPBOARD_MAX - 1) { + len = CLIPBOARD_MAX - 1; + } + + memcpy(sClipboard, text, len); + sClipboard[len] = '\0'; + sClipboardLen = len; +} + + +const char *clipboardGet(int32_t *outLen) { + if (outLen) { + *outLen = sClipboardLen; + } + + return sClipboard; +} + + +// ============================================================ +// Multi-click tracking +// ============================================================ + +static clock_t sLastClickTime = 0; +static int32_t sLastClickX = -1; +static int32_t sLastClickY = -1; +static int32_t sClickCount = 0; + +int32_t multiClickDetect(int32_t vx, int32_t vy) { + clock_t now = clock(); + + if ((now - sLastClickTime) < DBLCLICK_TICKS && + abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { + sClickCount++; + } else { + sClickCount = 1; + } + + sLastClickTime = now; + sLastClickX = vx; + sLastClickY = vy; + + return sClickCount; +} + + +bool isWordChar(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + + +int32_t wordStart(const char *buf, int32_t pos) { + while (pos > 0 && isWordChar(buf[pos - 1])) { + pos--; + } + + return pos; +} + + +int32_t wordEnd(const char *buf, int32_t len, int32_t pos) { + while (pos < len && isWordChar(buf[pos])) { + pos++; + } + + return pos; +} + + +// ============================================================ +// Clear selection on all text widgets except 'except' +// ============================================================ + +static bool clearSelectionsInTree(WidgetT *root, WidgetT *except) { + if (!root) { + return false; + } + + bool cleared = false; + WidgetT *stack[64]; + int32_t top = 0; + stack[top++] = root; + + while (top > 0) { + WidgetT *w = stack[--top]; + + if (w != except) { + if (w->type == WidgetTextInputE) { + if (w->as.textInput.selStart != w->as.textInput.selEnd) { + cleared = true; + } + + w->as.textInput.selStart = -1; + w->as.textInput.selEnd = -1; + } else if (w->type == WidgetTextAreaE) { + if (w->as.textArea.selAnchor != w->as.textArea.selCursor) { + cleared = true; + } + + w->as.textArea.selAnchor = -1; + w->as.textArea.selCursor = -1; + } else if (w->type == WidgetComboBoxE) { + if (w->as.comboBox.selStart != w->as.comboBox.selEnd) { + cleared = true; + } + + w->as.comboBox.selStart = -1; + w->as.comboBox.selEnd = -1; + } else if (w->type == WidgetAnsiTermE) { + if (w->as.ansiTerm.selStartLine >= 0 && + (w->as.ansiTerm.selStartLine != w->as.ansiTerm.selEndLine || + w->as.ansiTerm.selStartCol != w->as.ansiTerm.selEndCol)) { + cleared = true; + w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; + } + + w->as.ansiTerm.selStartLine = -1; + w->as.ansiTerm.selStartCol = -1; + w->as.ansiTerm.selEndLine = -1; + w->as.ansiTerm.selEndCol = -1; + w->as.ansiTerm.selecting = false; + } + } + + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + if (top < 64) { + stack[top++] = c; + } + } + } + + return cleared; +} + + +void clearOtherSelections(WidgetT *except) { + if (!except || !except->window || !except->window->widgetRoot) { + return; + } + + AppContextT *ctx = (AppContextT *)except->window->widgetRoot->userData; + + if (!ctx) { + return; + } + + for (int32_t i = 0; i < ctx->stack.count; i++) { + WindowT *win = ctx->stack.windows[i]; + + if (win && win->widgetRoot) { + if (clearSelectionsInTree(win->widgetRoot, except) && win != except->window) { + RectT fullRect = {0, 0, win->contentW, win->contentH}; + widgetOnPaint(win, &fullRect); + win->contentDirty = true; + dvxInvalidateWindow(ctx, win); + } + } + } +} + + +// ============================================================ +// 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; +} + + +// ============================================================ +// Shared selection helpers +// ============================================================ + +static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) { + int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd; + int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart; + + if (lo < 0) { + lo = 0; + } + + if (hi > *pLen) { + hi = *pLen; + } + + if (lo >= hi) { + *pSelStart = -1; + *pSelEnd = -1; + return; + } + + memmove(buf + lo, buf + hi, *pLen - hi + 1); + *pLen -= (hi - lo); + *pCursor = lo; + *pSelStart = -1; + *pSelEnd = -1; +} + // ============================================================ // wgtTextArea @@ -18,21 +259,18 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen) { if (w->as.textArea.buf) { w->as.textArea.buf[0] = '\0'; } + + w->as.textArea.undoBuf = (char *)malloc(bufSize); + w->as.textArea.selAnchor = -1; + w->as.textArea.selCursor = -1; + w->as.textArea.desiredCol = 0; + w->weight = 100; } return w; } -// ============================================================ -// widgetTextAreaDestroy -// ============================================================ - -void widgetTextAreaDestroy(WidgetT *w) { - free(w->as.textArea.buf); -} - - // ============================================================ // wgtTextInput // ============================================================ @@ -49,7 +287,10 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { w->as.textInput.buf[0] = '\0'; } - w->weight = 100; // text inputs stretch by default + w->as.textInput.undoBuf = (char *)malloc(bufSize); + w->as.textInput.selStart = -1; + w->as.textInput.selEnd = -1; + w->weight = 100; } return w; @@ -57,34 +298,1214 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) { // ============================================================ -// widgetTextInputDestroy +// TextArea line helpers // ============================================================ -void widgetTextInputDestroy(WidgetT *w) { - free(w->as.textInput.buf); +static int32_t textAreaCountLines(const char *buf, int32_t len) { + int32_t lines = 1; + + for (int32_t i = 0; i < len; i++) { + if (buf[i] == '\n') { + lines++; + } + } + + return lines; +} + + +static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) { + (void)len; + 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; +} + + +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 textAreaMaxLineLen(const char *buf, int32_t len) { + int32_t maxLen = 0; + int32_t curLen = 0; + + for (int32_t i = 0; i < len; i++) { + if (buf[i] == '\n') { + if (curLen > maxLen) { + maxLen = curLen; + } + curLen = 0; + } else { + curLen++; + } + } + + if (curLen > maxLen) { + maxLen = curLen; + } + + return maxLen; +} + + +static int32_t 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; +} + + +static void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols) { + int32_t row = w->as.textArea.cursorRow; + int32_t col = w->as.textArea.cursorCol; + + if (row < w->as.textArea.scrollRow) { + w->as.textArea.scrollRow = row; + } + + if (row >= w->as.textArea.scrollRow + visRows) { + w->as.textArea.scrollRow = row - visRows + 1; + } + + if (col < w->as.textArea.scrollCol) { + w->as.textArea.scrollCol = col; + } + + if (col >= w->as.textArea.scrollCol + visCols) { + w->as.textArea.scrollCol = col - visCols + 1; + } } // ============================================================ -// widgetTextInputGetText +// widgetTextAreaCalcMinSize // ============================================================ -const char *widgetTextInputGetText(const WidgetT *w) { - return w->as.textInput.buf ? w->as.textInput.buf : ""; +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; } // ============================================================ -// widgetTextInputSetText +// widgetTextAreaDestroy // ============================================================ -void widgetTextInputSetText(WidgetT *w, const char *text) { - if (w->as.textInput.buf) { - strncpy(w->as.textInput.buf, text, w->as.textInput.bufSize - 1); - w->as.textInput.buf[w->as.textInput.bufSize - 1] = '\0'; - w->as.textInput.len = (int32_t)strlen(w->as.textInput.buf); - w->as.textInput.cursorPos = w->as.textInput.len; - w->as.textInput.scrollOff = 0; +void widgetTextAreaDestroy(WidgetT *w) { + free(w->as.textArea.buf); + free(w->as.textArea.undoBuf); +} + + +// ============================================================ +// widgetTextAreaGetText +// ============================================================ + +const char *widgetTextAreaGetText(const WidgetT *w) { + return w->as.textArea.buf ? w->as.textArea.buf : ""; +} + + +// ============================================================ +// widgetTextAreaOnKey +// ============================================================ + +void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod) { + if (!w->as.textArea.buf) { + return; + } + + clearOtherSelections(w); + + char *buf = w->as.textArea.buf; + int32_t bufSize = w->as.textArea.bufSize; + int32_t *pLen = &w->as.textArea.len; + int32_t *pRow = &w->as.textArea.cursorRow; + int32_t *pCol = &w->as.textArea.cursorCol; + int32_t *pSA = &w->as.textArea.selAnchor; + int32_t *pSC = &w->as.textArea.selCursor; + bool shift = (mod & KEY_MOD_SHIFT) != 0; + + AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; + 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 = textAreaMaxLineLen(buf, *pLen); + 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 = textAreaCountLines(buf, *pLen); + + // 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) + + // Ctrl+A — select all + if (key == 1) { + *pSA = 0; + *pSC = *pLen; + textAreaOffToRowCol(buf, *pLen, pRow, pCol); + w->as.textArea.desiredCol = *pCol; + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Ctrl+C — copy + if (key == 3) { + if (HAS_SEL()) { + clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); + } + + return; + } + + // Ctrl+V — paste + if (key == 22) { + if (sClipboardLen > 0) { + textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.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); + textAreaOffToRowCol(buf, lo, pRow, pCol); + *pSA = -1; + *pSC = -1; + } + + int32_t off = CUR_OFF(); + int32_t canFit = bufSize - 1 - *pLen; + int32_t paste = sClipboardLen < canFit ? sClipboardLen : canFit; + + if (paste > 0) { + memmove(buf + off + paste, buf + off, *pLen - off + 1); + memcpy(buf + off, sClipboard, paste); + *pLen += paste; + textAreaOffToRowCol(buf, off + paste, pRow, pCol); + w->as.textArea.desiredCol = *pCol; + } + + if (w->onChange) { + w->onChange(w); + } + } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Ctrl+X — cut + if (key == 24) { + if (HAS_SEL()) { + clipboardCopy(buf + SEL_LO(), SEL_HI() - SEL_LO()); + textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); + + int32_t lo = SEL_LO(); + int32_t hi = SEL_HI(); + memmove(buf + lo, buf + hi, *pLen - hi + 1); + *pLen -= (hi - lo); + textAreaOffToRowCol(buf, lo, pRow, pCol); + *pSA = -1; + *pSC = -1; + w->as.textArea.desiredCol = *pCol; + + if (w->onChange) { + w->onChange(w); + } + } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Ctrl+Z — undo + if (key == 26) { + if (w->as.textArea.undoBuf && w->as.textArea.undoLen >= 0) { + // Swap current and undo + char tmpBuf[CLIPBOARD_MAX]; + 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 = w->as.textArea.undoLen < bufSize - 1 ? w->as.textArea.undoLen : bufSize - 1; + memcpy(buf, w->as.textArea.undoBuf, restLen); + buf[restLen] = '\0'; + *pLen = restLen; + + // Save current as new undo + int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; + memcpy(w->as.textArea.undoBuf, tmpBuf, saveLen); + w->as.textArea.undoBuf[saveLen] = '\0'; + w->as.textArea.undoLen = saveLen; + w->as.textArea.undoCursor = tmpCursor; + + // Restore cursor + int32_t restoreOff = w->as.textArea.undoCursor < *pLen ? w->as.textArea.undoCursor : *pLen; + w->as.textArea.undoCursor = tmpCursor; + textAreaOffToRowCol(buf, restoreOff, pRow, pCol); + w->as.textArea.desiredCol = *pCol; + *pSA = -1; + *pSC = -1; + + if (w->onChange) { + w->onChange(w); + } + } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Enter — insert newline + if (key == 0x0D) { + if (*pLen < bufSize - 1) { + textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.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); + textAreaOffToRowCol(buf, 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] = '\n'; + (*pLen)++; + (*pRow)++; + *pCol = 0; + w->as.textArea.desiredCol = 0; + } + + if (w->onChange) { + w->onChange(w); + } + } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Backspace + if (key == 8) { + if (HAS_SEL()) { + textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); + int32_t lo = SEL_LO(); + int32_t hi = SEL_HI(); + memmove(buf + lo, buf + hi, *pLen - hi + 1); + *pLen -= (hi - lo); + textAreaOffToRowCol(buf, lo, pRow, pCol); + *pSA = -1; + *pSC = -1; + w->as.textArea.desiredCol = *pCol; + + if (w->onChange) { + w->onChange(w); + } + } else { + int32_t off = CUR_OFF(); + + if (off > 0) { + textEditSaveUndo(buf, *pLen, off, w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); + memmove(buf + off - 1, buf + off, *pLen - off + 1); + (*pLen)--; + textAreaOffToRowCol(buf, off - 1, pRow, pCol); + w->as.textArea.desiredCol = *pCol; + + if (w->onChange) { + w->onChange(w); + } + } + } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Delete + if (key == (0x53 | 0x100)) { + if (HAS_SEL()) { + textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); + int32_t lo = SEL_LO(); + int32_t hi = SEL_HI(); + memmove(buf + lo, buf + hi, *pLen - hi + 1); + *pLen -= (hi - lo); + textAreaOffToRowCol(buf, lo, pRow, pCol); + *pSA = -1; + *pSC = -1; + w->as.textArea.desiredCol = *pCol; + + if (w->onChange) { + w->onChange(w); + } + } else { + int32_t off = CUR_OFF(); + + if (off < *pLen) { + textEditSaveUndo(buf, *pLen, off, w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.undoCursor, bufSize); + memmove(buf + off, buf + off + 1, *pLen - off); + (*pLen)--; + + if (w->onChange) { + w->onChange(w); + } + } + } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Left arrow + if (key == (0x4B | 0x100)) { + SEL_BEGIN(); + int32_t off = CUR_OFF(); + + if (off > 0) { + textAreaOffToRowCol(buf, off - 1, pRow, pCol); + } + + w->as.textArea.desiredCol = *pCol; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Right arrow + if (key == (0x4D | 0x100)) { + SEL_BEGIN(); + int32_t off = CUR_OFF(); + + if (off < *pLen) { + textAreaOffToRowCol(buf, off + 1, pRow, pCol); + } + + w->as.textArea.desiredCol = *pCol; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Up arrow + if (key == (0x48 | 0x100)) { + SEL_BEGIN(); + + if (*pRow > 0) { + (*pRow)--; + int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); + *pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; + } + + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Down arrow + if (key == (0x50 | 0x100)) { + SEL_BEGIN(); + + if (*pRow < totalLines - 1) { + (*pRow)++; + int32_t lineL = textAreaLineLen(buf, *pLen, *pRow); + *pCol = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; + } + + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Home + if (key == (0x47 | 0x100)) { + SEL_BEGIN(); + *pCol = 0; + w->as.textArea.desiredCol = 0; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // End + if (key == (0x4F | 0x100)) { + SEL_BEGIN(); + *pCol = textAreaLineLen(buf, *pLen, *pRow); + w->as.textArea.desiredCol = *pCol; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(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 = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(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 = w->as.textArea.desiredCol < lineL ? w->as.textArea.desiredCol : lineL; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Ctrl+Home (scancode 0x77) + if (key == (0x77 | 0x100)) { + SEL_BEGIN(); + *pRow = 0; + *pCol = 0; + w->as.textArea.desiredCol = 0; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Ctrl+End (scancode 0x75) + if (key == (0x75 | 0x100)) { + SEL_BEGIN(); + textAreaOffToRowCol(buf, *pLen, pRow, pCol); + w->as.textArea.desiredCol = *pCol; + SEL_END(); + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + // Printable character + if (key >= 32 && key < 127) { + if (*pLen < bufSize - 1) { + textEditSaveUndo(buf, *pLen, CUR_OFF(), w->as.textArea.undoBuf, &w->as.textArea.undoLen, &w->as.textArea.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); + textAreaOffToRowCol(buf, 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)++; + w->as.textArea.desiredCol = *pCol; + } + + if (w->onChange) { + w->onChange(w); + } + } + + textAreaEnsureVisible(w, visRows, visCols); + wgtInvalidate(w); + return; + } + + #undef CUR_OFF + #undef SEL_BEGIN + #undef SEL_END + #undef HAS_SEL + #undef SEL_LO + #undef SEL_HI +} + + +// ============================================================ +// widgetTextAreaOnMouse +// ============================================================ + +void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + w->focused = true; + clearOtherSelections(w); + + AppContextT *ctx = (AppContextT *)root->userData; + const BitmapFontT *font = &ctx->font; + + int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; + int32_t innerY = w->y + TEXTAREA_BORDER; + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t visCols = innerW / font->charWidth; + int32_t maxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len); + 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 = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len); + int32_t maxScroll = totalLines - visRows; + + if (maxScroll < 0) { + maxScroll = 0; + } + + w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); + + int32_t maxHScroll = maxLL - visCols; + + if (maxHScroll < 0) { + maxHScroll = 0; + } + + w->as.textArea.scrollCol = clampInt(w->as.textArea.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 (w->as.textArea.scrollCol > 0) { + w->as.textArea.scrollCol--; + } + } else if (relX >= hsbW - TEXTAREA_SB_W) { + // Right arrow + if (w->as.textArea.scrollCol < maxHScroll) { + w->as.textArea.scrollCol++; + } + } else if (trackLen > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, maxLL, visCols, w->as.textArea.scrollCol, &thumbPos, &thumbSize); + + int32_t trackRelX = relX - TEXTAREA_SB_W; + + if (trackRelX < thumbPos) { + w->as.textArea.scrollCol -= visCols; + w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll); + } else if (trackRelX >= thumbPos + thumbSize) { + w->as.textArea.scrollCol += visCols; + w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll); + } + } + + 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 (w->as.textArea.scrollRow > 0) { + w->as.textArea.scrollRow--; + } + } else if (relY >= sbH - TEXTAREA_SB_W) { + // Down arrow + if (w->as.textArea.scrollRow < maxScroll) { + w->as.textArea.scrollRow++; + } + } else if (trackLen > 0) { + int32_t thumbPos; + int32_t thumbSize; + widgetScrollbarThumb(trackLen, totalLines, visRows, w->as.textArea.scrollRow, &thumbPos, &thumbSize); + + int32_t trackRelY = relY - TEXTAREA_SB_W; + + if (trackRelY < thumbPos) { + w->as.textArea.scrollRow -= visRows; + w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); + } else if (trackRelY >= thumbPos + thumbSize) { + w->as.textArea.scrollRow += visRows; + w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); + } + } + + return; + } + + // Click on text area — place cursor + int32_t relX = vx - innerX; + int32_t relY = vy - innerY; + + int32_t clickRow = w->as.textArea.scrollRow + relY / font->charHeight; + int32_t clickCol = w->as.textArea.scrollCol + relX / font->charWidth; + + if (clickRow < 0) { + clickRow = 0; + } + + if (clickRow >= totalLines) { + clickRow = totalLines - 1; + } + + if (clickCol < 0) { + clickCol = 0; + } + + int32_t lineL = textAreaLineLen(w->as.textArea.buf, w->as.textArea.len, clickRow); + + if (clickCol > lineL) { + clickCol = lineL; + } + + int32_t clicks = multiClickDetect(vx, vy); + + if (clicks >= 3) { + // Triple-click: select entire line + int32_t lineStart = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, 0); + int32_t lineEnd = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, lineL); + + w->as.textArea.cursorRow = clickRow; + w->as.textArea.cursorCol = lineL; + w->as.textArea.desiredCol = lineL; + w->as.textArea.selAnchor = lineStart; + w->as.textArea.selCursor = lineEnd; + sDragTextSelect = NULL; + return; + } + + if (clicks == 2 && w->as.textArea.buf) { + // Double-click: select word + int32_t off = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); + int32_t ws = wordStart(w->as.textArea.buf, off); + int32_t we = wordEnd(w->as.textArea.buf, w->as.textArea.len, off); + + int32_t weRow; + int32_t weCol; + textAreaOffToRowCol(w->as.textArea.buf, we, &weRow, &weCol); + + w->as.textArea.cursorRow = weRow; + w->as.textArea.cursorCol = weCol; + w->as.textArea.desiredCol = weCol; + w->as.textArea.selAnchor = ws; + w->as.textArea.selCursor = we; + sDragTextSelect = NULL; + return; + } + + // Single click: place cursor + start drag-select + w->as.textArea.cursorRow = clickRow; + w->as.textArea.cursorCol = clickCol; + w->as.textArea.desiredCol = clickCol; + + int32_t anchorOff = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); + w->as.textArea.selAnchor = anchorOff; + w->as.textArea.selCursor = anchorOff; + sDragTextSelect = w; +} + + +// ============================================================ +// widgetTextDragUpdate — update selection during mouse drag +// ============================================================ + +void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { + AppContextT *ctx = (AppContextT *)root->userData; + const BitmapFontT *font = &ctx->font; + + if (w->type == WidgetTextInputE) { + int32_t leftEdge = w->x + TEXT_INPUT_PAD; + int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; + int32_t rightEdge = leftEdge + maxChars * font->charWidth; + + // Auto-scroll when mouse is past edges + if (vx < leftEdge && w->as.textInput.scrollOff > 0) { + w->as.textInput.scrollOff--; + } else if (vx >= rightEdge && w->as.textInput.scrollOff + maxChars < w->as.textInput.len) { + w->as.textInput.scrollOff++; + } + + int32_t relX = vx - leftEdge; + int32_t charPos = relX / font->charWidth + w->as.textInput.scrollOff; + + if (charPos < 0) { + charPos = 0; + } + + if (charPos > w->as.textInput.len) { + charPos = w->as.textInput.len; + } + + w->as.textInput.cursorPos = charPos; + w->as.textInput.selEnd = charPos; + } else if (w->type == WidgetTextAreaE) { + int32_t innerX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; + int32_t innerY = w->y + TEXTAREA_BORDER; + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t visCols = innerW / font->charWidth; + int32_t maxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len); + 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 = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len); + + if (visRows < 1) { + visRows = 1; + } + + if (visCols < 1) { + visCols = 1; + } + + // Auto-scroll vertically + if (vy < innerY && w->as.textArea.scrollRow > 0) { + w->as.textArea.scrollRow--; + } else if (vy >= innerY + visRows * font->charHeight && w->as.textArea.scrollRow + visRows < totalLines) { + w->as.textArea.scrollRow++; + } + + // Auto-scroll horizontally + int32_t rightEdge = innerX + visCols * font->charWidth; + + if (vx < innerX && w->as.textArea.scrollCol > 0) { + w->as.textArea.scrollCol--; + } else if (vx >= rightEdge && w->as.textArea.scrollCol < maxLL - visCols) { + w->as.textArea.scrollCol++; + } + + int32_t relX = vx - innerX; + int32_t relY = vy - innerY; + + int32_t clickRow = w->as.textArea.scrollRow + relY / font->charHeight; + int32_t clickCol = w->as.textArea.scrollCol + relX / font->charWidth; + + if (clickRow < 0) { + clickRow = 0; + } + + if (clickRow >= totalLines) { + clickRow = totalLines - 1; + } + + if (clickCol < 0) { + clickCol = 0; + } + + int32_t lineL = textAreaLineLen(w->as.textArea.buf, w->as.textArea.len, clickRow); + + if (clickCol > lineL) { + clickCol = lineL; + } + + w->as.textArea.cursorRow = clickRow; + w->as.textArea.cursorCol = clickCol; + w->as.textArea.desiredCol = clickCol; + w->as.textArea.selCursor = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); + } else if (w->type == WidgetComboBoxE) { + int32_t leftEdge = w->x + TEXT_INPUT_PAD; + int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth; + int32_t rightEdge = leftEdge + maxChars * font->charWidth; + + // Auto-scroll when mouse is past edges + if (vx < leftEdge && w->as.comboBox.scrollOff > 0) { + w->as.comboBox.scrollOff--; + } else if (vx >= rightEdge && w->as.comboBox.scrollOff + maxChars < w->as.comboBox.len) { + w->as.comboBox.scrollOff++; + } + + int32_t relX = vx - leftEdge; + int32_t charPos = relX / font->charWidth + w->as.comboBox.scrollOff; + + if (charPos < 0) { + charPos = 0; + } + + if (charPos > w->as.comboBox.len) { + charPos = w->as.comboBox.len; + } + + w->as.comboBox.cursorPos = charPos; + w->as.comboBox.selEnd = charPos; + } else if (w->type == WidgetAnsiTermE) { + int32_t baseX = w->x + 2; // ANSI_BORDER + int32_t baseY = w->y + 2; + int32_t cols = w->as.ansiTerm.cols; + int32_t rows = w->as.ansiTerm.rows; + + int32_t clickRow = (vy - baseY) / font->charHeight; + int32_t clickCol = (vx - baseX) / font->charWidth; + + if (clickRow < 0) { + clickRow = 0; + } + + if (clickRow >= rows) { + clickRow = rows - 1; + } + + if (clickCol < 0) { + clickCol = 0; + } + + if (clickCol >= cols) { + clickCol = cols; + } + + w->as.ansiTerm.selEndLine = w->as.ansiTerm.scrollPos + clickRow; + w->as.ansiTerm.selEndCol = clickCol; + w->as.ansiTerm.dirtyRows = 0xFFFFFFFF; + } +} + + +// ============================================================ +// widgetTextAreaPaint +// ============================================================ + +void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; + uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; + + char *buf = w->as.textArea.buf; + int32_t len = w->as.textArea.len; + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; + int32_t visCols = innerW / font->charWidth; + int32_t maxLL = textAreaMaxLineLen(buf, len); + 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 = textAreaCountLines(buf, len); + 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; + } + + w->as.textArea.scrollRow = clampInt(w->as.textArea.scrollRow, 0, maxScroll); + + // Clamp horizontal scroll + int32_t maxHScroll = maxLL - visCols; + + if (maxHScroll < 0) { + maxHScroll = 0; + } + + w->as.textArea.scrollCol = clampInt(w->as.textArea.scrollCol, 0, maxHScroll); + + // Selection range + int32_t selLo = -1; + int32_t selHi = -1; + + if (w->as.textArea.selAnchor >= 0 && w->as.textArea.selCursor >= 0 && w->as.textArea.selAnchor != w->as.textArea.selCursor) { + selLo = w->as.textArea.selAnchor < w->as.textArea.selCursor ? w->as.textArea.selAnchor : w->as.textArea.selCursor; + selHi = w->as.textArea.selAnchor < w->as.textArea.selCursor ? w->as.textArea.selCursor : w->as.textArea.selAnchor; + } + + // Draw lines + int32_t textX = w->x + TEXTAREA_BORDER + TEXTAREA_PAD; + int32_t textY = w->y + TEXTAREA_BORDER; + + for (int32_t i = 0; i < visRows; i++) { + int32_t row = w->as.textArea.scrollRow + i; + + if (row >= totalLines) { + break; + } + + int32_t lineOff = textAreaLineStart(buf, len, row); + int32_t lineL = textAreaLineLen(buf, len, row); + int32_t drawY = textY + i * font->charHeight; + + for (int32_t j = 0; j < visCols; j++) { + int32_t col = w->as.textArea.scrollCol + j; + int32_t charOff = lineOff + col; + int32_t drawX = textX + j * font->charWidth; + + uint32_t cfgc = fg; + uint32_t cbgc = bg; + + // Check selection — past end of line, test the newline byte + // instead of lineOff+col (which aliases into subsequent lines) + bool inSel = false; + if (selLo >= 0) { + if (col < lineL) { + inSel = (charOff >= selLo && charOff < selHi); + } else { + int32_t nlOff = lineOff + lineL; + inSel = (nlOff >= selLo && nlOff < selHi); + } + } + + if (inSel) { + cfgc = colors->menuHighlightFg; + cbgc = colors->menuHighlightBg; + } + + if (col < lineL) { + drawChar(d, ops, font, drawX, drawY, buf[charOff], cfgc, cbgc, true); + } else if (inSel) { + rectFill(d, ops, drawX, drawY, font->charWidth, font->charHeight, cbgc); + } + } + } + + // Draw cursor + if (w->focused) { + int32_t curDrawCol = w->as.textArea.cursorCol - w->as.textArea.scrollCol; + int32_t curDrawRow = w->as.textArea.cursorRow - w->as.textArea.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, w->as.textArea.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, w->as.textArea.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->focused) { + drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, fg); + } +} + + +// ============================================================ +// widgetTextAreaSetText +// ============================================================ + +void widgetTextAreaSetText(WidgetT *w, const char *text) { + if (w->as.textArea.buf) { + strncpy(w->as.textArea.buf, text, w->as.textArea.bufSize - 1); + w->as.textArea.buf[w->as.textArea.bufSize - 1] = '\0'; + w->as.textArea.len = (int32_t)strlen(w->as.textArea.buf); + w->as.textArea.cursorRow = 0; + w->as.textArea.cursorCol = 0; + w->as.textArea.scrollRow = 0; + w->as.textArea.scrollCol = 0; + w->as.textArea.desiredCol = 0; + w->as.textArea.selAnchor = -1; + w->as.textArea.selCursor = -1; } } @@ -99,15 +1520,53 @@ void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font) { } +// ============================================================ +// widgetTextInputDestroy +// ============================================================ + +void widgetTextInputDestroy(WidgetT *w) { + free(w->as.textInput.buf); + free(w->as.textInput.undoBuf); +} + + +// ============================================================ +// widgetTextInputGetText +// ============================================================ + +const char *widgetTextInputGetText(const WidgetT *w) { + return w->as.textInput.buf ? w->as.textInput.buf : ""; +} + + +// ============================================================ +// widgetTextInputOnKey +// ============================================================ + +void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) { + if (!w->as.textInput.buf) { + return; + } + + clearOtherSelections(w); + + widgetTextEditOnKey(w, key, mod, w->as.textInput.buf, w->as.textInput.bufSize, + &w->as.textInput.len, &w->as.textInput.cursorPos, + &w->as.textInput.scrollOff, + &w->as.textInput.selStart, &w->as.textInput.selEnd, + w->as.textInput.undoBuf, &w->as.textInput.undoLen, + &w->as.textInput.undoCursor); +} + + // ============================================================ // widgetTextInputOnMouse // ============================================================ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { - (void)vy; w->focused = true; + clearOtherSelections(w); - // Place cursor at click position AppContextT *ctx = (AppContextT *)root->userData; const BitmapFontT *font = &ctx->font; int32_t relX = vx - w->x - TEXT_INPUT_PAD; @@ -121,107 +1580,34 @@ void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { charPos = w->as.textInput.len; } - w->as.textInput.cursorPos = charPos; -} + int32_t clicks = multiClickDetect(vx, vy); - -// ============================================================ -// widgetTextEditOnKey — shared text editing logic -// ============================================================ - -void widgetTextEditOnKey(WidgetT *w, int32_t key, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff) { - if (key >= 32 && key < 127) { - // Printable character - if (*pLen < bufSize - 1) { - int32_t pos = *pCursor; - - memmove(buf + pos + 1, buf + pos, *pLen - pos + 1); - buf[pos] = (char)key; - (*pLen)++; - (*pCursor)++; - - if (w->onChange) { - w->onChange(w); - } - } - } else if (key == 8) { - // Backspace - if (*pCursor > 0) { - int32_t pos = *pCursor; - - memmove(buf + pos - 1, buf + pos, *pLen - pos + 1); - (*pLen)--; - (*pCursor)--; - - if (w->onChange) { - w->onChange(w); - } - } - } else if (key == (0x4B | 0x100)) { - // Left arrow - if (*pCursor > 0) { - (*pCursor)--; - } - } else if (key == (0x4D | 0x100)) { - // Right arrow - if (*pCursor < *pLen) { - (*pCursor)++; - } - } else if (key == (0x47 | 0x100)) { - // Home - *pCursor = 0; - } else if (key == (0x4F | 0x100)) { - // End - *pCursor = *pLen; - } else if (key == (0x53 | 0x100)) { - // Delete - if (*pCursor < *pLen) { - int32_t pos = *pCursor; - - memmove(buf + pos, buf + pos + 1, *pLen - pos); - (*pLen)--; - - if (w->onChange) { - w->onChange(w); - } - } - } - - // Adjust scroll offset to keep cursor visible - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - int32_t fieldW = w->w; - - if (w->type == WidgetComboBoxE) { - fieldW -= DROPDOWN_BTN_WIDTH; - } - - int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth; - - if (*pCursor < *pScrollOff) { - *pScrollOff = *pCursor; - } - - if (*pCursor >= *pScrollOff + visibleChars) { - *pScrollOff = *pCursor - visibleChars + 1; - } - - wgtInvalidate(w); -} - - -// ============================================================ -// widgetTextInputOnKey -// ============================================================ - -void widgetTextInputOnKey(WidgetT *w, int32_t key) { - if (!w->as.textInput.buf) { + if (clicks >= 3) { + // Triple-click: select all (single line) + w->as.textInput.selStart = 0; + w->as.textInput.selEnd = w->as.textInput.len; + w->as.textInput.cursorPos = w->as.textInput.len; + sDragTextSelect = NULL; return; } - widgetTextEditOnKey(w, key, w->as.textInput.buf, w->as.textInput.bufSize, - &w->as.textInput.len, &w->as.textInput.cursorPos, - &w->as.textInput.scrollOff); + if (clicks == 2 && w->as.textInput.buf) { + // Double-click: select word + int32_t ws = wordStart(w->as.textInput.buf, charPos); + int32_t we = wordEnd(w->as.textInput.buf, w->as.textInput.len, charPos); + + w->as.textInput.selStart = ws; + w->as.textInput.selEnd = we; + w->as.textInput.cursorPos = we; + sDragTextSelect = NULL; + return; + } + + // Single click: place cursor + start drag-select + w->as.textInput.cursorPos = charPos; + w->as.textInput.selStart = charPos; + w->as.textInput.selEnd = charPos; + sDragTextSelect = w; } @@ -253,10 +1639,27 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi len = maxChars; } + // Selection range + int32_t selLo = -1; + int32_t selHi = -1; + + if (w->as.textInput.selStart >= 0 && w->as.textInput.selEnd >= 0 && w->as.textInput.selStart != w->as.textInput.selEnd) { + selLo = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selStart : w->as.textInput.selEnd; + selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart; + } + for (int32_t i = 0; i < len; i++) { + int32_t charIdx = off + i; + uint32_t cfgc = fg; + uint32_t cbgc = bg; + + if (selLo >= 0 && charIdx >= selLo && charIdx < selHi) { + cfgc = colors->menuHighlightFg; + cbgc = colors->menuHighlightBg; + } + drawChar(d, ops, font, textX + i * font->charWidth, textY, - w->as.textInput.buf[off + i], - fg, bg, true); + w->as.textInput.buf[charIdx], cfgc, cbgc, true); } // Draw cursor @@ -270,3 +1673,350 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi } } } + + +// ============================================================ +// widgetTextInputSetText +// ============================================================ + +void widgetTextInputSetText(WidgetT *w, const char *text) { + if (w->as.textInput.buf) { + strncpy(w->as.textInput.buf, text, w->as.textInput.bufSize - 1); + w->as.textInput.buf[w->as.textInput.bufSize - 1] = '\0'; + w->as.textInput.len = (int32_t)strlen(w->as.textInput.buf); + w->as.textInput.cursorPos = w->as.textInput.len; + w->as.textInput.scrollOff = 0; + w->as.textInput.selStart = -1; + w->as.textInput.selEnd = -1; + } +} + + +// ============================================================ +// widgetTextEditOnKey — shared single-line text editing logic +// ============================================================ + +void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) { + bool shift = (mod & KEY_MOD_SHIFT) != 0; + bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd); + int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1; + int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1; + + // Ctrl+A — select all + if (key == 1 && pSelStart && pSelEnd) { + *pSelStart = 0; + *pSelEnd = *pLen; + *pCursor = *pLen; + goto adjustScroll; + } + + // Ctrl+C — copy + if (key == 3) { + if (hasSel) { + clipboardCopy(buf + selLo, selHi - selLo); + } + + return; + } + + // Ctrl+V — paste + if (key == 22) { + if (sClipboardLen > 0) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + if (hasSel) { + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + } + + int32_t canFit = bufSize - 1 - *pLen; + // For single-line, skip newlines in clipboard + int32_t paste = 0; + + for (int32_t i = 0; i < sClipboardLen && paste < canFit; i++) { + if (sClipboard[i] != '\n' && sClipboard[i] != '\r') { + paste++; + } + } + + if (paste > 0) { + int32_t pos = *pCursor; + memmove(buf + pos + paste, buf + pos, *pLen - pos + 1); + + int32_t j = 0; + + for (int32_t i = 0; i < sClipboardLen && j < paste; i++) { + if (sClipboard[i] != '\n' && sClipboard[i] != '\r') { + buf[pos + j] = sClipboard[i]; + j++; + } + } + + *pLen += paste; + *pCursor += paste; + } + + if (w->onChange) { + w->onChange(w); + } + } + + goto adjustScroll; + } + + // Ctrl+X — cut + if (key == 24) { + if (hasSel) { + clipboardCopy(buf + selLo, selHi - selLo); + + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + + if (w->onChange) { + w->onChange(w); + } + } + + goto adjustScroll; + } + + // Ctrl+Z — undo + if (key == 26 && undoBuf && pUndoLen && pUndoCursor) { + // Swap current and undo + char tmpBuf[CLIPBOARD_MAX]; + int32_t tmpLen = *pLen; + int32_t tmpCursor = *pCursor; + int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1; + + memcpy(tmpBuf, buf, copyLen); + tmpBuf[copyLen] = '\0'; + + int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1; + memcpy(buf, undoBuf, restLen); + buf[restLen] = '\0'; + *pLen = restLen; + + int32_t restoreCursor = *pUndoCursor < *pLen ? *pUndoCursor : *pLen; + + // Save old as new undo + int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; + memcpy(undoBuf, tmpBuf, saveLen); + undoBuf[saveLen] = '\0'; + *pUndoLen = saveLen; + *pUndoCursor = tmpCursor; + + *pCursor = restoreCursor; + + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + if (w->onChange) { + w->onChange(w); + } + + goto adjustScroll; + } + + if (key >= 32 && key < 127) { + // Printable character + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + if (hasSel) { + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + } + + if (*pLen < bufSize - 1) { + int32_t pos = *pCursor; + memmove(buf + pos + 1, buf + pos, *pLen - pos + 1); + buf[pos] = (char)key; + (*pLen)++; + (*pCursor)++; + + if (w->onChange) { + w->onChange(w); + } + } + } else if (key == 8) { + // Backspace + if (hasSel) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + + if (w->onChange) { + w->onChange(w); + } + } else if (*pCursor > 0) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + int32_t pos = *pCursor; + memmove(buf + pos - 1, buf + pos, *pLen - pos + 1); + (*pLen)--; + (*pCursor)--; + + if (w->onChange) { + w->onChange(w); + } + } + } else if (key == (0x4B | 0x100)) { + // Left arrow + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + if (*pCursor > 0) { + (*pCursor)--; + } + + *pSelEnd = *pCursor; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + if (*pCursor > 0) { + (*pCursor)--; + } + } + } else if (key == (0x4D | 0x100)) { + // Right arrow + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + if (*pCursor < *pLen) { + (*pCursor)++; + } + + *pSelEnd = *pCursor; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + if (*pCursor < *pLen) { + (*pCursor)++; + } + } + } else if (key == (0x47 | 0x100)) { + // Home + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = 0; + *pSelEnd = 0; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = 0; + } + } else if (key == (0x4F | 0x100)) { + // End + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = *pLen; + *pSelEnd = *pLen; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = *pLen; + } + } else if (key == (0x53 | 0x100)) { + // Delete + if (hasSel) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + + if (w->onChange) { + w->onChange(w); + } + } else if (*pCursor < *pLen) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + int32_t pos = *pCursor; + memmove(buf + pos, buf + pos + 1, *pLen - pos); + (*pLen)--; + + if (w->onChange) { + w->onChange(w); + } + } + } else { + return; + } + +adjustScroll: + // Adjust scroll offset to keep cursor visible + { + AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; + const BitmapFontT *font = &ctx->font; + int32_t fieldW = w->w; + + if (w->type == WidgetComboBoxE) { + fieldW -= DROPDOWN_BTN_WIDTH; + } + + int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth; + + if (*pCursor < *pScrollOff) { + *pScrollOff = *pCursor; + } + + if (*pCursor >= *pScrollOff + visibleChars) { + *pScrollOff = *pCursor - visibleChars + 1; + } + } + + wgtInvalidate(w); +} diff --git a/dvx/widgets/widgetTreeView.c b/dvx/widgets/widgetTreeView.c index 59584fa..362307d 100644 --- a/dvx/widgets/widgetTreeView.c +++ b/dvx/widgets/widgetTreeView.c @@ -649,7 +649,9 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) { // widgetTreeViewOnKey // ============================================================ -void widgetTreeViewOnKey(WidgetT *w, int32_t key) { +void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod) { + (void)mod; + if (!w || w->type != WidgetTreeViewE) { return; } diff --git a/dvxdemo/demo.c b/dvxdemo/demo.c index 78182c4..5a966cf 100644 --- a/dvxdemo/demo.c +++ b/dvxdemo/demo.c @@ -4,7 +4,11 @@ #include "dvxWidget.h" #include +#include #include +#include + +#include "thirdparty/stb_image.h" // ============================================================ // Menu command IDs @@ -25,6 +29,7 @@ // Prototypes // ============================================================ +static uint8_t *loadBmpPixels(AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch); static void onCloseCb(WindowT *win); static void onCloseMainCb(WindowT *win); static void onMenuCb(WindowT *win, int32_t menuId); @@ -45,6 +50,54 @@ static void setupWidgetDemo(AppContextT *ctx); static AppContextT *sCtx = NULL; +// ============================================================ +// loadBmpPixels — load a BMP/PNG file into display-format pixels +// ============================================================ + +static uint8_t *loadBmpPixels(AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) { + int imgW; + int imgH; + int channels; + uint8_t *rgb = stbi_load(path, &imgW, &imgH, &channels, 3); + + if (!rgb) { + return NULL; + } + + const DisplayT *d = dvxGetDisplay(ctx); + int32_t bpp = d->format.bytesPerPixel; + int32_t pitch = imgW * bpp; + uint8_t *data = (uint8_t *)malloc(pitch * imgH); + + if (!data) { + stbi_image_free(rgb); + return NULL; + } + + for (int32_t y = 0; y < imgH; y++) { + for (int32_t x = 0; x < imgW; x++) { + uint8_t *src = rgb + (y * imgW + x) * 3; + uint32_t color = packColor(d, src[0], src[1], src[2]); + uint8_t *dst = data + y * pitch + x * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } + + stbi_image_free(rgb); + *outW = imgW; + *outH = imgH; + *outPitch = pitch; + return data; +} + + // ============================================================ // onCloseCb // ============================================================ @@ -308,7 +361,7 @@ static const char *colorItems[] = {"Red", "Green", "Blue", "Yellow", "Cyan", "Ma static const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"}; static void setupControlsWindow(AppContextT *ctx) { - WindowT *win = dvxCreateWindow(ctx, "Advanced Widgets", 380, 50, 320, 400, true); + WindowT *win = dvxCreateWindow(ctx, "Advanced Widgets", 380, 50, 360, 440, true); if (!win) { return; @@ -366,18 +419,79 @@ static void setupControlsWindow(AppContextT *ctx) { wgtTreeItem(config, "settings.ini"); wgtTreeItem(config, "palette.dat"); - // --- Tab 3: Toolbar --- + // --- Tab 3: Toolbar (ImageButtons + VSeparator) --- WidgetT *page3 = wgtTabPage(tabs, "Tool&bar"); WidgetT *tb = wgtToolbar(page3); - WidgetT *btnNew = wgtButton(tb, "&New"); - WidgetT *btnOpen = wgtButton(tb, "&Open"); - WidgetT *btnSave = wgtButton(tb, "&Save"); - btnNew->onClick = onToolbarClick; - btnOpen->onClick = onToolbarClick; - btnSave->onClick = onToolbarClick; - wgtLabel(page3, "Toolbar with buttons above."); + int32_t imgW; + int32_t imgH; + int32_t imgPitch; + + uint8_t *newData = loadBmpPixels(ctx, "new.bmp", &imgW, &imgH, &imgPitch); + if (newData) { + WidgetT *btnNew = wgtImageButton(tb, newData, imgW, imgH, imgPitch); + strncpy(btnNew->name, "New", MAX_WIDGET_NAME); + btnNew->onClick = onToolbarClick; + } + + uint8_t *openData = loadBmpPixels(ctx, "open.bmp", &imgW, &imgH, &imgPitch); + if (openData) { + WidgetT *btnOpen = wgtImageButton(tb, openData, imgW, imgH, imgPitch); + strncpy(btnOpen->name, "Open", MAX_WIDGET_NAME); + btnOpen->onClick = onToolbarClick; + } + + uint8_t *saveData = loadBmpPixels(ctx, "save.bmp", &imgW, &imgH, &imgPitch); + if (saveData) { + WidgetT *btnSave = wgtImageButton(tb, saveData, imgW, imgH, imgPitch); + strncpy(btnSave->name, "Save", MAX_WIDGET_NAME); + btnSave->onClick = onToolbarClick; + } + + wgtVSeparator(tb); + WidgetT *btnHelp = wgtButton(tb, "&Help"); + btnHelp->onClick = onToolbarClick; + + wgtLabel(page3, "ImageButtons with VSeparator."); + + // --- Tab 4: Media (Image, ImageFromFile) --- + WidgetT *page4 = wgtTabPage(tabs, "&Media"); + + wgtLabel(page4, "ImageFromFile (sample.bmp):"); + wgtImageFromFile(page4, "sample.bmp"); + + wgtHSeparator(page4); + + wgtLabel(page4, "Image (logo.bmp):"); + WidgetT *imgRow = wgtHBox(page4); + uint8_t *logoData = loadBmpPixels(ctx, "logo.bmp", &imgW, &imgH, &imgPitch); + if (logoData) { + wgtImage(imgRow, logoData, imgW, imgH, imgPitch); + } + wgtVSeparator(imgRow); + wgtLabel(imgRow, "32x32 DV/X logo"); + + // --- Tab 5: Editor (TextArea, Canvas) --- + WidgetT *page5 = wgtTabPage(tabs, "&Editor"); + + wgtLabel(page5, "TextArea:"); + WidgetT *ta = wgtTextArea(page5, 512); + ta->weight = 100; + wgtSetText(ta, "Multi-line text editor.\n\nFeatures:\n- Word wrap\n- Selection\n- Copy/Paste\n- Undo (Ctrl+Z)"); + + wgtHSeparator(page5); + + wgtLabel(page5, "Canvas (draw with mouse):"); + const DisplayT *d = dvxGetDisplay(ctx); + WidgetT *cv = wgtCanvas(page5, 280, 80); + wgtCanvasSetPenColor(cv, packColor(d, 200, 0, 0)); + wgtCanvasDrawRect(cv, 5, 5, 50, 35); + wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 200)); + wgtCanvasFillCircle(cv, 150, 40, 25); + wgtCanvasSetPenColor(cv, packColor(d, 0, 150, 0)); + wgtCanvasDrawLine(cv, 70, 5, 130, 70); + wgtCanvasSetPenColor(cv, packColor(d, 0, 0, 0)); // Status bar at bottom (outside tabs) WidgetT *sb = wgtStatusBar(root); @@ -630,7 +744,23 @@ static void setupWidgetDemo(AppContextT *ctx) { // main // ============================================================ -int main(void) { +int main(int argc, char **argv) { + (void)argc; + + // Change to executable's directory so relative BMP paths work + char exeDir[260]; + strncpy(exeDir, argv[0], sizeof(exeDir) - 1); + exeDir[sizeof(exeDir) - 1] = '\0'; + char *lastSep = strrchr(exeDir, '/'); + char *lastBs = strrchr(exeDir, '\\'); + if (lastBs > lastSep) { + lastSep = lastBs; + } + if (lastSep) { + *lastSep = '\0'; + chdir(exeDir); + } + AppContextT ctx; printf("DV/X GUI Demo\n"); diff --git a/dvxdemo/logo.bmp b/dvxdemo/logo.bmp new file mode 100644 index 0000000..4633ea9 --- /dev/null +++ b/dvxdemo/logo.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d98dd5f0f63db53421f60bc35813df103572f7e9622671934f4a7b4ab3c9154 +size 3126 diff --git a/dvxdemo/new.bmp b/dvxdemo/new.bmp new file mode 100644 index 0000000..50e8fef --- /dev/null +++ b/dvxdemo/new.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c602b729d75f14f36bccd84b27810bc7e04f4b8566d5311eee029ed76ed971e +size 822 diff --git a/dvxdemo/open.bmp b/dvxdemo/open.bmp new file mode 100644 index 0000000..6d2f732 --- /dev/null +++ b/dvxdemo/open.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2791595909626da80d820eadc1334e0dfb356194f58f2cce42919d7d5c9e1018 +size 822 diff --git a/dvxdemo/sample.bmp b/dvxdemo/sample.bmp new file mode 100644 index 0000000..e0dde10 --- /dev/null +++ b/dvxdemo/sample.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:667f64ede12cd76d3089adb7680c9b90ea192580297bf2d170e0035221e28a8f +size 6966 diff --git a/dvxdemo/save.bmp b/dvxdemo/save.bmp new file mode 100644 index 0000000..d033c2c --- /dev/null +++ b/dvxdemo/save.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd8eb75b0e7dea67eafa87fde8dd2106b603986c5cc6ae6be511ae8f27958fc8 +size 822