Added masked editing and password entry.
This commit is contained in:
parent
fd41390085
commit
52a2730ccb
4 changed files with 427 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,3 +5,4 @@ lib/
|
|||
*.~
|
||||
.gitignore~
|
||||
DVX_GUI_DESIGN.md
|
||||
*.SWP
|
||||
|
|
|
|||
|
|
@ -94,6 +94,16 @@ typedef enum {
|
|||
FrameFlatE // solid color line
|
||||
} 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
|
||||
// ============================================================
|
||||
|
|
@ -177,16 +187,18 @@ typedef struct WidgetT {
|
|||
} radio;
|
||||
|
||||
struct {
|
||||
char *buf;
|
||||
int32_t bufSize;
|
||||
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;
|
||||
char *buf;
|
||||
int32_t bufSize;
|
||||
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;
|
||||
InputModeE inputMode;
|
||||
const char *mask; // format mask for InputMaskedE
|
||||
} textInput;
|
||||
|
||||
struct {
|
||||
|
|
@ -385,6 +397,8 @@ WidgetT *wgtLabel(WidgetT *parent, const char *text);
|
|||
WidgetT *wgtButton(WidgetT *parent, const char *text);
|
||||
WidgetT *wgtCheckbox(WidgetT *parent, const char *text);
|
||||
WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen);
|
||||
WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen);
|
||||
WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask);
|
||||
|
||||
// ============================================================
|
||||
// Radio buttons
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@
|
|||
// 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 textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col);
|
||||
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
|
||||
// ============================================================
|
||||
|
|
@ -1550,6 +1925,18 @@ void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|||
|
||||
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,
|
||||
&w->as.textInput.len, &w->as.textInput.cursorPos,
|
||||
&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;
|
||||
}
|
||||
|
||||
bool isPassword = (w->as.textInput.inputMode == InputPasswordE);
|
||||
|
||||
for (int32_t i = 0; i < len; i++) {
|
||||
int32_t charIdx = off + i;
|
||||
uint32_t cfgc = fg;
|
||||
|
|
@ -1658,8 +2047,14 @@ void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bi
|
|||
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,
|
||||
w->as.textInput.buf[charIdx], cfgc, cbgc, true);
|
||||
displayCh, cfgc, cbgc, true);
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
|
|
|
|||
|
|
@ -700,8 +700,12 @@ static void setupWidgetDemo(AppContextT *ctx) {
|
|||
wgtTextInput(row1, 64);
|
||||
|
||||
WidgetT *row2 = wgtHBox(frame);
|
||||
wgtLabel(row2, "A&ddress:");
|
||||
wgtTextInput(row2, 64);
|
||||
wgtLabel(row2, "&Password:");
|
||||
wgtPasswordInput(row2, 32);
|
||||
|
||||
WidgetT *row3 = wgtHBox(frame);
|
||||
wgtLabel(row3, "P&hone:");
|
||||
wgtMaskedInput(row3, "(###) ###-####");
|
||||
|
||||
wgtHSeparator(root);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue