Much improved text editing. More widget demos.

This commit is contained in:
Scott Duensing 2026-03-14 17:50:38 -05:00
parent 505b95ff8c
commit fd41390085
25 changed files with 2558 additions and 218 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
*.bmp filter=lfs diff=lfs merge=lfs -text
*.BMP filter=lfs diff=lfs merge=lfs -text

View file

@ -10,6 +10,7 @@
#include <ctype.h> #include <ctype.h>
#include <time.h> #include <time.h>
#include <dpmi.h> #include <dpmi.h>
#include <signal.h>
#define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2) #define DBLCLICK_THRESHOLD (CLOCKS_PER_SEC / 2)
#define ICON_REFRESH_INTERVAL 8 #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) { int32_t dvxInit(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
memset(ctx, 0, sizeof(*ctx)); memset(ctx, 0, sizeof(*ctx));
// Disable Ctrl+C/Break termination
signal(SIGINT, SIG_IGN);
// Initialize video // Initialize video
if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) { if (videoInit(&ctx->display, requestedW, requestedH, preferredBpp) != 0) {
return -1; return -1;
@ -1935,7 +1939,7 @@ static void pollKeyboard(AppContextT *ctx) {
if (termFocused) { if (termFocused) {
// Terminal has focus — send Tab to it // Terminal has focus — send Tab to it
if (win->onKey) { if (win->onKey) {
win->onKey(win, ascii ? ascii : (scancode | 0x100), 0); win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags);
} }
continue; continue;
@ -1994,7 +1998,7 @@ static void pollKeyboard(AppContextT *ctx) {
WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx]; WindowT *win = ctx->stack.windows[ctx->stack.focusedIdx];
if (win->onKey) { if (win->onKey) {
win->onKey(win, ascii ? ascii : (scancode | 0x100), 0); win->onKey(win, ascii ? ascii : (scancode | 0x100), shiftFlags);
} }
} }

View file

@ -182,6 +182,11 @@ typedef struct WidgetT {
int32_t len; int32_t len;
int32_t cursorPos; int32_t cursorPos;
int32_t scrollOff; 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; } textInput;
struct { struct {
@ -191,6 +196,13 @@ typedef struct WidgetT {
int32_t cursorRow; int32_t cursorRow;
int32_t cursorCol; int32_t cursorCol;
int32_t scrollRow; 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; } textArea;
struct { struct {
@ -225,6 +237,11 @@ typedef struct WidgetT {
int32_t len; int32_t len;
int32_t cursorPos; int32_t cursorPos;
int32_t scrollOff; 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; const char **items;
int32_t itemCount; int32_t itemCount;
int32_t selectedIdx; int32_t selectedIdx;
@ -330,6 +347,12 @@ typedef struct WidgetT {
// Cached packed palette (avoids packColor per repaint) // Cached packed palette (avoids packColor per repaint)
uint32_t packedPalette[16]; uint32_t packedPalette[16];
bool paletteValid; 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) // Communications interface (all NULL = disconnected)
void *commCtx; void *commCtx;
int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen);

View file

@ -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 ansiTermAddToScrollback(WidgetT *w, int32_t screenRow);
static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d); 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 ansiTermDeleteLines(WidgetT *w, int32_t count);
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count); static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count);
static void ansiTermDirtyRow(WidgetT *w, int32_t row); 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 ansiTermEraseLine(WidgetT *w, int32_t mode);
static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count); static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count);
static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex); 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 ansiTermInsertLines(WidgetT *w, int32_t count);
static void ansiTermNewline(WidgetT *w); 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 ansiTermProcessByte(WidgetT *w, uint8_t ch);
static void ansiTermProcessSgr(WidgetT *w); static void ansiTermProcessSgr(WidgetT *w);
static void ansiTermPutChar(WidgetT *w, uint8_t ch); static void ansiTermPutChar(WidgetT *w, uint8_t ch);
static void ansiTermScrollDown(WidgetT *w); static void ansiTermScrollDown(WidgetT *w);
static void ansiTermScrollUp(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 // 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 // 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 // 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 // wgtAnsiTerm
// ============================================================ // ============================================================
@ -1031,6 +1181,11 @@ WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) {
w->as.ansiTerm.commCtx = NULL; w->as.ansiTerm.commCtx = NULL;
w->as.ansiTerm.commRead = NULL; w->as.ansiTerm.commRead = NULL;
w->as.ansiTerm.commWrite = 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.blinkVisible = true;
w->as.ansiTerm.blinkTime = clock(); w->as.ansiTerm.blinkTime = clock();
w->as.ansiTerm.cursorOn = true; 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); 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 < minRow) { minRow = row; }
if (row > maxRow) { maxRow = row; } if (row > maxRow) { maxRow = row; }
@ -1308,6 +1464,8 @@ void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) {
return; return;
} }
ansiTermClearSelection(w);
for (int32_t i = 0; i < len; i++) { for (int32_t i = 0; i < len; i++) {
ansiTermProcessByte(w, data[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 // widgetAnsiTermOnKey
// ============================================================ // ============================================================
@ -1341,11 +1547,35 @@ void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// Translate keyboard input to ANSI escape sequences and send // Translate keyboard input to ANSI escape sequences and send
// via the comm interface. Does nothing if commWrite is NULL. // 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) { if (!w->as.ansiTerm.commWrite) {
return; return;
} }
// Any keypress clears selection
if (ansiTermHasSelection(w)) {
ansiTermClearSelection(w);
}
uint8_t buf[8]; uint8_t buf[8];
int32_t len = 0; int32_t len = 0;
@ -1405,6 +1635,10 @@ void widgetAnsiTermOnKey(WidgetT *w, int32_t key) {
// Delete → ESC[3~ // Delete → ESC[3~
buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~';
len = 4; 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) { if (len > 0) {
@ -1425,14 +1659,8 @@ void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy)
AppContextT *actx = (AppContextT *)root->userData; AppContextT *actx = (AppContextT *)root->userData;
const BitmapFontT *font = &actx->font; const BitmapFontT *font = &actx->font;
hit->focused = true; 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 cols = hit->as.ansiTerm.cols;
int32_t rows = hit->as.ansiTerm.rows; int32_t rows = hit->as.ansiTerm.rows;
int32_t sbX = hit->x + ANSI_BORDER + cols * font->charWidth; 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 sbH = rows * font->charHeight;
int32_t arrowH = ANSI_SB_W; 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; 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); 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 // Draw scrollbar

View file

@ -53,7 +53,9 @@ void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetButtonOnKey // widgetButtonOnKey
// ============================================================ // ============================================================
void widgetButtonOnKey(WidgetT *w, int32_t key) { void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
if (key == ' ' || key == 0x0D) { if (key == ' ' || key == 0x0D) {
w->as.button.pressed = true; w->as.button.pressed = true;
sKeyPressedBtn = w; sKeyPressedBtn = w;

View file

@ -54,7 +54,9 @@ void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetCheckboxOnKey // widgetCheckboxOnKey
// ============================================================ // ============================================================
void widgetCheckboxOnKey(WidgetT *w, int32_t key) { void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
if (key == ' ' || key == 0x0D) { if (key == ' ' || key == 0x0D) {
w->as.checkbox.checked = !w->as.checkbox.checked; w->as.checkbox.checked = !w->as.checkbox.checked;

View file

@ -111,16 +111,16 @@ static const WidgetClassT sClassTextInput = {
}; };
static const WidgetClassT sClassTextArea = { static const WidgetClassT sClassTextArea = {
.flags = 0, .flags = WCLASS_FOCUSABLE,
.paint = NULL, .paint = widgetTextAreaPaint,
.paintOverlay = NULL, .paintOverlay = NULL,
.calcMinSize = NULL, .calcMinSize = widgetTextAreaCalcMinSize,
.layout = NULL, .layout = NULL,
.onMouse = NULL, .onMouse = widgetTextAreaOnMouse,
.onKey = NULL, .onKey = widgetTextAreaOnKey,
.destroy = widgetTextAreaDestroy, .destroy = widgetTextAreaDestroy,
.getText = NULL, .getText = widgetTextAreaGetText,
.setText = NULL .setText = widgetTextAreaSetText
}; };
static const WidgetClassT sClassListBox = { static const WidgetClassT sClassListBox = {

View file

@ -19,6 +19,9 @@ WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) {
w->as.comboBox.buf[0] = '\0'; 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->as.comboBox.selectedIdx = -1;
w->weight = 100; 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.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len; w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0; w->as.comboBox.scrollOff = 0;
} w->as.comboBox.selStart = -1;
} w->as.comboBox.selEnd = -1;
// ============================================================
// 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;
} }
} }
@ -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 // widgetComboBoxOnKey
// ============================================================ // ============================================================
void widgetComboBoxOnKey(WidgetT *w, int32_t key) { void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) {
if (w->as.comboBox.open) { if (w->as.comboBox.open) {
if (key == (0x48 | 0x100)) { if (key == (0x48 | 0x100)) {
if (w->as.comboBox.hoverIdx > 0) { 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.len = (int32_t)strlen(w->as.comboBox.buf);
w->as.comboBox.cursorPos = w->as.comboBox.len; w->as.comboBox.cursorPos = w->as.comboBox.len;
w->as.comboBox.scrollOff = 0; w->as.comboBox.scrollOff = 0;
w->as.comboBox.selStart = -1;
w->as.comboBox.selEnd = -1;
} }
w->as.comboBox.open = false; w->as.comboBox.open = false;
@ -214,9 +207,14 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key) {
return; 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.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 widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
(void)vy;
w->focused = true; w->focused = true;
// Check if click is on the button area // 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; sOpenPopup = w->as.comboBox.open ? w : NULL;
} else { } else {
// Text area click — focus for editing // Text area click — focus for editing
clearOtherSelections(w);
AppContextT *ctx = (AppContextT *)root->userData; AppContextT *ctx = (AppContextT *)root->userData;
const BitmapFontT *font = &ctx->font; const BitmapFontT *font = &ctx->font;
int32_t relX = vx - w->x - TEXT_INPUT_PAD; 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; 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.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; 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++) { 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, 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 // 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); 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;
}
}

View file

@ -11,6 +11,7 @@ WidgetT *sOpenPopup = NULL;
WidgetT *sPressedButton = NULL; WidgetT *sPressedButton = NULL;
WidgetT *sDragSlider = NULL; WidgetT *sDragSlider = NULL;
WidgetT *sDrawingCanvas = NULL; WidgetT *sDrawingCanvas = NULL;
WidgetT *sDragTextSelect = NULL;
int32_t sDragOffset = 0; int32_t sDragOffset = 0;

View file

@ -101,7 +101,9 @@ void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetDropdownOnKey // 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) { if (w->as.dropdown.open) {
// Popup is open — navigate items // Popup is open — navigate items
if (key == (0x48 | 0x100)) { if (key == (0x48 | 0x100)) {

View file

@ -108,7 +108,6 @@ void widgetManageScrollbars(WindowT *win, AppContextT *ctx) {
// ============================================================ // ============================================================
void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
(void)mod;
WidgetT *root = win->widgetRoot; WidgetT *root = win->widgetRoot;
if (!root) { if (!root) {
@ -143,7 +142,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) {
// Dispatch to per-widget onKey handler via vtable // Dispatch to per-widget onKey handler via vtable
if (focus->wclass && focus->wclass->onKey) { 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; 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 // Handle canvas drawing release
if (sDrawingCanvas && !(buttons & 1)) { if (sDrawingCanvas && !(buttons & 1)) {
sDrawingCanvas->as.canvas.lastX = -1; sDrawingCanvas->as.canvas.lastX = -1;

View file

@ -71,7 +71,9 @@ void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetImageButtonOnKey // widgetImageButtonOnKey
// ============================================================ // ============================================================
void widgetImageButtonOnKey(WidgetT *w, int32_t key) { void widgetImageButtonOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
if (key == ' ' || key == 0x0D) { if (key == ' ' || key == 0x0D) {
w->as.imageButton.pressed = true; w->as.imageButton.pressed = true;
sKeyPressedBtn = w; sKeyPressedBtn = w;

View file

@ -29,7 +29,7 @@ typedef struct WidgetClassT {
void (*calcMinSize)(WidgetT *w, const BitmapFontT *font); void (*calcMinSize)(WidgetT *w, const BitmapFontT *font);
void (*layout)(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 (*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); void (*destroy)(WidgetT *w);
const char *(*getText)(const WidgetT *w); const char *(*getText)(const WidgetT *w);
void (*setText)(WidgetT *w, const char *text); void (*setText)(WidgetT *w, const char *text);
@ -41,6 +41,11 @@ extern const WidgetClassT *widgetClassTable[];
// Constants // 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_SPACING 4
#define DEFAULT_PADDING 4 #define DEFAULT_PADDING 4
#define SEPARATOR_THICKNESS 2 #define SEPARATOR_THICKNESS 2
@ -87,6 +92,7 @@ extern WidgetT *sOpenPopup;
extern WidgetT *sPressedButton; extern WidgetT *sPressedButton;
extern WidgetT *sDragSlider; extern WidgetT *sDragSlider;
extern WidgetT *sDrawingCanvas; extern WidgetT *sDrawingCanvas;
extern WidgetT *sDragTextSelect;
extern int32_t sDragOffset; 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 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 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 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 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 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); 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 widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font); void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTabControlCalcMinSize(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 widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font);
void widgetTreeViewCalcMinSize(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); void widgetLabelSetText(WidgetT *w, const char *text);
const char *widgetRadioGetText(const WidgetT *w); const char *widgetRadioGetText(const WidgetT *w);
void widgetRadioSetText(WidgetT *w, const char *text); 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); const char *widgetTextInputGetText(const WidgetT *w);
void widgetTextInputSetText(WidgetT *w, const char *text); void widgetTextInputSetText(WidgetT *w, const char *text);
const char *widgetTreeItemGetText(const WidgetT *w); const char *widgetTreeItemGetText(const WidgetT *w);
@ -230,32 +240,42 @@ void widgetTextInputDestroy(WidgetT *w);
// Per-widget mouse/key functions // 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 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 widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetCanvasOnMouse(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 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 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 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 widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
void widgetImageOnMouse(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 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 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 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 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 widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod);
void widgetTextInputOnKey(WidgetT *w, int32_t key); 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 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); void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
#endif // WIDGET_INTERNAL_H #endif // WIDGET_INTERNAL_H

View file

@ -95,7 +95,9 @@ void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetListBoxOnKey // 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) { if (!w || w->type != WidgetListBoxE || w->as.listBox.itemCount == 0) {
return; return;
} }

View file

@ -79,7 +79,9 @@ void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetRadioOnKey // widgetRadioOnKey
// ============================================================ // ============================================================
void widgetRadioOnKey(WidgetT *w, int32_t key) { void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
if (key == ' ' || key == 0x0D) { if (key == ' ' || key == 0x0D) {
// Select this radio // Select this radio
if (w->parent && w->parent->type == WidgetRadioGroupE) { if (w->parent && w->parent->type == WidgetRadioGroupE) {

View file

@ -77,7 +77,9 @@ void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetSliderOnKey // 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 step = 1;
int32_t range = w->as.slider.maxValue - w->as.slider.minValue; int32_t range = w->as.slider.maxValue - w->as.slider.minValue;

View file

@ -131,7 +131,9 @@ void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
// widgetTabControlOnKey // widgetTabControlOnKey
// ============================================================ // ============================================================
void widgetTabControlOnKey(WidgetT *w, int32_t key) { void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
(void)mod;
int32_t tabCount = 0; int32_t tabCount = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {

File diff suppressed because it is too large Load diff

View file

@ -649,7 +649,9 @@ void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font) {
// widgetTreeViewOnKey // 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) { if (!w || w->type != WidgetTreeViewE) {
return; return;
} }

View file

@ -4,7 +4,11 @@
#include "dvxWidget.h" #include "dvxWidget.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h>
#include "thirdparty/stb_image.h"
// ============================================================ // ============================================================
// Menu command IDs // Menu command IDs
@ -25,6 +29,7 @@
// Prototypes // 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 onCloseCb(WindowT *win);
static void onCloseMainCb(WindowT *win); static void onCloseMainCb(WindowT *win);
static void onMenuCb(WindowT *win, int32_t menuId); static void onMenuCb(WindowT *win, int32_t menuId);
@ -45,6 +50,54 @@ static void setupWidgetDemo(AppContextT *ctx);
static AppContextT *sCtx = NULL; 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 // 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 const char *sizeItems[] = {"Small", "Medium", "Large", "Extra Large"};
static void setupControlsWindow(AppContextT *ctx) { 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) { if (!win) {
return; return;
@ -366,18 +419,79 @@ static void setupControlsWindow(AppContextT *ctx) {
wgtTreeItem(config, "settings.ini"); wgtTreeItem(config, "settings.ini");
wgtTreeItem(config, "palette.dat"); wgtTreeItem(config, "palette.dat");
// --- Tab 3: Toolbar --- // --- Tab 3: Toolbar (ImageButtons + VSeparator) ---
WidgetT *page3 = wgtTabPage(tabs, "Tool&bar"); WidgetT *page3 = wgtTabPage(tabs, "Tool&bar");
WidgetT *tb = wgtToolbar(page3); 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) // Status bar at bottom (outside tabs)
WidgetT *sb = wgtStatusBar(root); WidgetT *sb = wgtStatusBar(root);
@ -630,7 +744,23 @@ static void setupWidgetDemo(AppContextT *ctx) {
// main // 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; AppContextT ctx;
printf("DV/X GUI Demo\n"); printf("DV/X GUI Demo\n");

BIN
dvxdemo/logo.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dvxdemo/new.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dvxdemo/open.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dvxdemo/sample.bmp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
dvxdemo/save.bmp (Stored with Git LFS) Normal file

Binary file not shown.