Added masked editing and password entry.

This commit is contained in:
Scott Duensing 2026-03-14 18:03:31 -05:00
parent fd41390085
commit 52a2730ccb
4 changed files with 427 additions and 13 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ lib/
*.~ *.~
.gitignore~ .gitignore~
DVX_GUI_DESIGN.md DVX_GUI_DESIGN.md
*.SWP

View file

@ -94,6 +94,16 @@ typedef enum {
FrameFlatE // solid color line FrameFlatE // solid color line
} FrameStyleE; } FrameStyleE;
// ============================================================
// Text input mode enum
// ============================================================
typedef enum {
InputNormalE, // default free-form text
InputPasswordE, // displays bullets, no copy
InputMaskedE // format mask (e.g. "(###) ###-####")
} InputModeE;
// ============================================================ // ============================================================
// Widget structure // Widget structure
// ============================================================ // ============================================================
@ -187,6 +197,8 @@ typedef struct WidgetT {
char *undoBuf; char *undoBuf;
int32_t undoLen; int32_t undoLen;
int32_t undoCursor; int32_t undoCursor;
InputModeE inputMode;
const char *mask; // format mask for InputMaskedE
} textInput; } textInput;
struct { struct {
@ -385,6 +397,8 @@ WidgetT *wgtLabel(WidgetT *parent, const char *text);
WidgetT *wgtButton(WidgetT *parent, const char *text); WidgetT *wgtButton(WidgetT *parent, const char *text);
WidgetT *wgtCheckbox(WidgetT *parent, const char *text); WidgetT *wgtCheckbox(WidgetT *parent, const char *text);
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen);
WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen);
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask);
// ============================================================ // ============================================================
// Radio buttons // Radio buttons

View file

@ -17,6 +17,12 @@
// Prototypes // Prototypes
// ============================================================ // ============================================================
static bool maskCharValid(char slot, char ch);
static int32_t maskFirstSlot(const char *mask);
static bool maskIsSlot(char ch);
static int32_t maskNextSlot(const char *mask, int32_t pos);
static int32_t maskPrevSlot(const char *mask, int32_t pos);
static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod);
static int32_t textAreaCountLines(const char *buf, int32_t len); 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 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 void textAreaEnsureVisible(WidgetT *w, int32_t visRows, int32_t visCols);
@ -297,6 +303,375 @@ WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen) {
} }
WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen) {
WidgetT *w = wgtTextInput(parent, maxLen);
if (w) {
w->as.textInput.inputMode = InputPasswordE;
}
return w;
}
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask) {
if (!mask) {
return NULL;
}
int32_t maskLen = (int32_t)strlen(mask);
WidgetT *w = wgtTextInput(parent, maskLen);
if (w) {
w->as.textInput.inputMode = InputMaskedE;
w->as.textInput.mask = mask;
// Pre-fill buffer: literals copied as-is, slots filled with '_'
for (int32_t i = 0; i < maskLen; i++) {
if (maskIsSlot(mask[i])) {
w->as.textInput.buf[i] = '_';
} else {
w->as.textInput.buf[i] = mask[i];
}
}
w->as.textInput.buf[maskLen] = '\0';
w->as.textInput.len = maskLen;
w->as.textInput.cursorPos = maskFirstSlot(mask);
}
return w;
}
// ============================================================
// Mask helpers
// ============================================================
static bool maskIsSlot(char ch) {
return ch == '#' || ch == 'A' || ch == '*';
}
static bool maskCharValid(char slot, char ch) {
switch (slot) {
case '#':
return ch >= '0' && ch <= '9';
case 'A':
return isalpha((unsigned char)ch);
case '*':
return ch >= 32 && ch < 127;
default:
return false;
}
}
static int32_t maskFirstSlot(const char *mask) {
for (int32_t i = 0; mask[i]; i++) {
if (maskIsSlot(mask[i])) {
return i;
}
}
return 0;
}
static int32_t maskNextSlot(const char *mask, int32_t pos) {
for (int32_t i = pos + 1; mask[i]; i++) {
if (maskIsSlot(mask[i])) {
return i;
}
}
return (int32_t)strlen(mask);
}
static int32_t maskPrevSlot(const char *mask, int32_t pos) {
for (int32_t i = pos - 1; i >= 0; i--) {
if (maskIsSlot(mask[i])) {
return i;
}
}
return pos;
}
static void maskedInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
char *buf = w->as.textInput.buf;
const char *mask = w->as.textInput.mask;
int32_t *pCur = &w->as.textInput.cursorPos;
int32_t maskLen = w->as.textInput.len;
bool shift = (mod & KEY_MOD_SHIFT) != 0;
(void)shift;
// Ctrl+A — select all
if (key == 1) {
w->as.textInput.selStart = 0;
w->as.textInput.selEnd = maskLen;
*pCur = maskLen;
goto done;
}
// Ctrl+C — copy formatted text
if (key == 3) {
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;
clipboardCopy(buf + selLo, selHi - selLo);
}
return;
}
// Ctrl+V — paste valid chars into slots
if (key == 22) {
int32_t clipLen;
const char *clip = clipboardGet(&clipLen);
if (clipLen > 0) {
if (w->as.textInput.undoBuf) {
textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize);
}
int32_t slotPos = *pCur;
bool changed = false;
for (int32_t i = 0; i < clipLen && slotPos < maskLen; i++) {
// Skip to next slot if not on one
while (slotPos < maskLen && !maskIsSlot(mask[slotPos])) {
slotPos++;
}
if (slotPos >= maskLen) {
break;
}
// Skip non-matching clipboard chars
if (maskCharValid(mask[slotPos], clip[i])) {
buf[slotPos] = clip[i];
slotPos = maskNextSlot(mask, slotPos);
changed = true;
}
}
if (changed) {
*pCur = slotPos;
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
if (w->onChange) {
w->onChange(w);
}
}
}
goto done;
}
// Ctrl+X — copy and clear selected slots
if (key == 24) {
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;
clipboardCopy(buf + selLo, selHi - selLo);
if (w->as.textInput.undoBuf) {
textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize);
}
for (int32_t i = selLo; i < selHi; i++) {
if (maskIsSlot(mask[i])) {
buf[i] = '_';
}
}
*pCur = selLo;
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
if (w->onChange) {
w->onChange(w);
}
}
goto done;
}
// Ctrl+Z — undo
if (key == 26 && w->as.textInput.undoBuf) {
char tmpBuf[CLIPBOARD_MAX];
int32_t tmpCursor = *pCur;
memcpy(tmpBuf, buf, maskLen + 1);
memcpy(buf, w->as.textInput.undoBuf, maskLen + 1);
*pCur = w->as.textInput.undoCursor < maskLen ? w->as.textInput.undoCursor : maskLen;
memcpy(w->as.textInput.undoBuf, tmpBuf, maskLen + 1);
w->as.textInput.undoCursor = tmpCursor;
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
if (w->onChange) {
w->onChange(w);
}
goto done;
}
if (key >= 32 && key < 127) {
// Printable character — place at current slot if valid
if (*pCur < maskLen && maskIsSlot(mask[*pCur]) && maskCharValid(mask[*pCur], (char)key)) {
if (w->as.textInput.undoBuf) {
textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize);
}
buf[*pCur] = (char)key;
*pCur = maskNextSlot(mask, *pCur);
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == 8) {
// Backspace — clear previous slot
int32_t prev = maskPrevSlot(mask, *pCur);
if (prev != *pCur) {
if (w->as.textInput.undoBuf) {
textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize);
}
buf[prev] = '_';
*pCur = prev;
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == (0x53 | 0x100)) {
// Delete — clear current slot
if (*pCur < maskLen && maskIsSlot(mask[*pCur])) {
if (w->as.textInput.undoBuf) {
textEditSaveUndo(buf, maskLen, *pCur, w->as.textInput.undoBuf, &w->as.textInput.undoLen, &w->as.textInput.undoCursor, w->as.textInput.bufSize);
}
buf[*pCur] = '_';
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
if (w->onChange) {
w->onChange(w);
}
}
} else if (key == (0x4B | 0x100)) {
// Left arrow — move to previous slot
int32_t prev = maskPrevSlot(mask, *pCur);
if (shift) {
if (w->as.textInput.selStart < 0) {
w->as.textInput.selStart = *pCur;
w->as.textInput.selEnd = *pCur;
}
*pCur = prev;
w->as.textInput.selEnd = *pCur;
} else {
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
*pCur = prev;
}
} else if (key == (0x4D | 0x100)) {
// Right arrow — move to next slot
int32_t next = maskNextSlot(mask, *pCur);
if (shift) {
if (w->as.textInput.selStart < 0) {
w->as.textInput.selStart = *pCur;
w->as.textInput.selEnd = *pCur;
}
*pCur = next;
w->as.textInput.selEnd = *pCur;
} else {
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
*pCur = next;
}
} else if (key == (0x47 | 0x100)) {
// Home — first slot
if (shift) {
if (w->as.textInput.selStart < 0) {
w->as.textInput.selStart = *pCur;
w->as.textInput.selEnd = *pCur;
}
*pCur = maskFirstSlot(mask);
w->as.textInput.selEnd = *pCur;
} else {
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
*pCur = maskFirstSlot(mask);
}
} else if (key == (0x4F | 0x100)) {
// End — past last slot
int32_t last = maskLen;
if (shift) {
if (w->as.textInput.selStart < 0) {
w->as.textInput.selStart = *pCur;
w->as.textInput.selEnd = *pCur;
}
*pCur = last;
w->as.textInput.selEnd = *pCur;
} else {
w->as.textInput.selStart = -1;
w->as.textInput.selEnd = -1;
*pCur = last;
}
} else {
return;
}
done:
// Adjust scroll
{
AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData;
const BitmapFontT *font = &ctx->font;
int32_t visibleChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth;
if (*pCur < w->as.textInput.scrollOff) {
w->as.textInput.scrollOff = *pCur;
}
if (*pCur >= w->as.textInput.scrollOff + visibleChars) {
w->as.textInput.scrollOff = *pCur - visibleChars + 1;
}
}
wgtInvalidate(w);
}
// ============================================================ // ============================================================
// TextArea line helpers // TextArea line helpers
// ============================================================ // ============================================================
@ -1550,6 +1925,18 @@ void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
clearOtherSelections(w); clearOtherSelections(w);
if (w->as.textInput.inputMode == InputMaskedE) {
maskedInputOnKey(w, key, mod);
return;
}
// Password mode: block copy (Ctrl+C) and cut (Ctrl+X)
if (w->as.textInput.inputMode == InputPasswordE) {
if (key == 3 || key == 24) {
return;
}
}
widgetTextEditOnKey(w, key, mod, w->as.textInput.buf, w->as.textInput.bufSize, widgetTextEditOnKey(w, key, mod, w->as.textInput.buf, w->as.textInput.bufSize,
&w->as.textInput.len, &w->as.textInput.cursorPos, &w->as.textInput.len, &w->as.textInput.cursorPos,
&w->as.textInput.scrollOff, &w->as.textInput.scrollOff,
@ -1648,6 +2035,8 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart; selHi = w->as.textInput.selStart < w->as.textInput.selEnd ? w->as.textInput.selEnd : w->as.textInput.selStart;
} }
bool isPassword = (w->as.textInput.inputMode == InputPasswordE);
for (int32_t i = 0; i < len; i++) { for (int32_t i = 0; i < len; i++) {
int32_t charIdx = off + i; int32_t charIdx = off + i;
uint32_t cfgc = fg; uint32_t cfgc = fg;
@ -1658,8 +2047,14 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
cbgc = colors->menuHighlightBg; cbgc = colors->menuHighlightBg;
} }
char displayCh = w->as.textInput.buf[charIdx];
if (isPassword) {
displayCh = '\xF9'; // CP437 bullet
}
drawChar(d, ops, font, textX + i * font->charWidth, textY, drawChar(d, ops, font, textX + i * font->charWidth, textY,
w->as.textInput.buf[charIdx], cfgc, cbgc, true); displayCh, cfgc, cbgc, true);
} }
// Draw cursor // Draw cursor

View file

@ -700,8 +700,12 @@ static void setupWidgetDemo(AppContextT *ctx) {
wgtTextInput(row1, 64); wgtTextInput(row1, 64);
WidgetT *row2 = wgtHBox(frame); WidgetT *row2 = wgtHBox(frame);
wgtLabel(row2, "A&ddress:"); wgtLabel(row2, "&Password:");
wgtTextInput(row2, 64); wgtPasswordInput(row2, 32);
WidgetT *row3 = wgtHBox(frame);
wgtLabel(row3, "P&hone:");
wgtMaskedInput(row3, "(###) ###-####");
wgtHSeparator(root); wgtHSeparator(root);